C# для профессионалов

Том II

Глава 13

XML

XML играет очень большую роль в платформе .NET. Платформа не только позволяет использовать XML в приложениях, но сама применяет XML для таких вещей, как конфигурационные файлы и документация исходного кода. Для XML платформа .NET содержит пространство имен System.Xml. Это пространство имен загружается вместе с классами, задействованными при обработке XML.

В этой главе говорится о том, как использовать реализацию DOM, и что предлагает .NET в качестве замены SAX. Будет показана совместная работа XML и ADO.NET и их преобразования. Мы узнаем так же, как можно сериализовать объекты в документ XML и создать объект из документа XML (десериализовать). Кроме того увидим, как включать XML в приложения C#. Следующие классы будут рассмотрены более подробно:

□ XmlReader и XmlTextReader

□ XmlWriter и XmlTextWriter

□ XmlDocument и DOM

□ XPath и XslTransform

□ ADO.NET и XmlDataDocument

□ XmlSerialization

Начнем эту главу с текущего состояния стандартов XML.

Стандарты W3C

Консорциум WWW (W3C) разработал множество стандартов, придающих XML мощь и потенциал. Без этих стандартов XML не смог бы оказать такого большого влияния на мир разработки. В этой книге не будут подробно рассматриваться тонкости XML. Для этого необходимо использовать другие источники. Среди книг Wrox, переведенных издательством "Лори", можно порекомендовать "Введение в XML" (2001 г., 656 стр.), "XML для профессионалов" (2001 г., 896 стр.) и "The XML Handbook" (ISBN 0-13-055068). Конечно, web-сайт W3C является ценным источником информации о XML (www.w3.org). В мае 2001 г. платформа .NET поддерживала следующие стандарты:

□ XML 1.0 — www.w3.org/TR/1998/REC-XML-19980210 — включая поддержку DTD (XmlTextReader).

□ Пространства имен XML — www.w3.org/TR/REC-xml-names — уровень потока и DOM.

□ Схемы XML — www.w3.org/TR/xmlschema-1 — поддерживается отображение схем и сериализация, но пока еще не поддерживается проверка.

□ Выражения XPath — www.w3.org/TR/xpath

□ Преобразования XSL/T — www.w3.org/TR/xslt

□ Ядро DOM Level 2 — www.w3.org/TR/DOM-Level-2

□ Soap 1.1 — msdn.microsoft.com/xml/general/soapspec.asp

Уровень поддержки стандартов будет меняться по мере развития платформы и по мере того, как W3C продолжает обновлять рекомендованные стандарты. В связи с этим необходимо всегда оставаться на современном уровне стандартов и уровне поддержки, который обеспечивает Microsoft.

Пространство имен System.Xml

Рассмотрим (без определенного порядка) некоторые классы пространства имен System.Xml.

Имя класса

Описание

XmlReader

Абстрактный. Средство чтения, которое предоставляет быстрый, некэшированный доступ к данным XML. XmlReader читает только вперед, аналогично синтаксическому анализатору SAX.

XmlWriter

Абстрактный. Средство записи, которое предоставляет быструю, некэшированную запись данных XML в поток или файл.

XmlTextReader

Реализует XmlReader. Предоставляет быстрый потоковый доступ для чтения с режимом только вперед к данным XML. Разрешает (допускает) использование данных в одном представлении.

XmlTextWriter

Реализует XmlWriter. Быстрая генерация потоков записи XML с режимом только вперед.

XmlNode

Абстрактный. Класс, который представляет единичный узел в документе XML. Базовый класс для нескольких классов в пространстве имен XML.

XmlDocument

Реализует XmlNode. Объектная модель документов W3C (DOM, Document Object Model). Задает в памяти представление документа XML в виде дерева, разрешая перемещение и редактирование.

XmlDataDocument

Реализует XmlDocument. То есть документ, который можно загрузить из данных XML или из реляционных данных объекта DataSet из ADO.NET.

XmlResolver

Абстрактный. Разрешает внешние ресурсы на основе XML, такие как DTD и схемные ссылки. Используется также для обработки элементов <xsl:include> и <xsl:import>.

XmlUrlResolver

Реализует XmlResolver. Разрешает внешние ресурсы с помощью URI (унифицированный идентификатор ресурса).

XML является также частью пространства имен System.Data в классе DataSet.

Имя класса

Описание

ReadXml

Считывает данные XML и схему в DataSet.

ReadXmlSchema

Считывает схему XML в DataSet.

WriteXml

Переписывает XML и схему из DataSet в документ XML.

WriteXmlSchema

Переписывает схему из DataSet в документ XML.

Необходимо отметить, что эта книга посвящена языку C#, поэтому все примеры будут написаны на C#. Однако пространство имен XML доступно в любом языке, который является частью семейства .NET. Это означает, что все приведенные примеры могли быть также написаны на языках VB.NET, Управляемый C++ и т.д.

XML 3.0 (MSXML3.DLL) в C#

Как быть, если имеется большой объем кода, разработанного с помощью синтаксического анализатора компании Microsoft (в настоящее время XML 3.0)? Придется ли его выбросить и начать все сначала? А что если вам удобно использовать объектную модель XML 3.0 DOM? Нужно ли немедленно переключаться на .NET?

Ответом будет — нет. XML 3.0 может использоваться непосредственно в приложениях. Если добавить ссылку на msxml3.DLL в свое решение, то можно будет начать писать некоторый код.

Следующие несколько примеров будут использовать файл books.xml в качестве источника данных. Его можно загрузить с web-сайта издательства Wrox, он также включен в несколько примеров .NET SDK. Файл books.xml является каталогом книг воображаемого книжного склада. Он содержит такую информацию, как жанр, имя автора, цена и номер ISBN. Все примеры кода в этой главе также доступны на web-сайте издательства Wrox: www.wrox.com. Чтобы выполнить эти примеры, файлы данных XML должны находиться в структуре путей, которая выглядит примерно следующим образом:

/XMLChapter/Sample1

/XMLChapter/Sample2

/XMLChapter/Sample3

и т. д. Файлы XML должны находиться в подкаталоге XMLChapter, а код для примеров должен быть в подкаталогах Sample1, Sample2 и т.д. Можно называть каталоги как угодно, но их относительное положение важно. Можно также изменять примеры, чтобы указать желаемое направление. В коде примеров будут сделаны указания, какие строки изменить.

Файл books.xml выглядит следующим образом:

<?xml version='1.0'?>

<!-- Этот файл представляет фрагмент базы данных учета запасов книжного склада -->

<bookstore>

<book genre="autobiography" publicationdate="1981" ISBN="1-861003-11-0">

<title>The Autobiography of Benjamin Franklin</title>

<author>

<first-name>Benjamin</first-name>

<last-name>Franklin</last-name>

</author>

<price>8.99</price>

</book>

<book genre="novel" publicationdate="1967" ISBN="0-201-63361-2">

<title>The Confidence Man</title>

<author>

<first-name>Herman</first-name>

<last-name>Melville</last-name>

</author>

<price>11.99</price>

</book>

<book genre="philosophy" publicationdate="1991" ISBN="1-861001-57-6">

<title>The Gorgias</title>

<author>

<name>Plato</name>

</author>

<price>9.99</price>

</book>

</bookstore>

Рассмотрим пример кода, использующего MSXML 3.0 для загрузки окна списка с номерами ISBN из books.xml. Ниже записан код, который можно найти в папке SampleBase1 архива, загружаемого с web-сайта издательства Wrox. Можно скопировать его в Visual Studio IDE или создать новую форму Windows Form с самого начала. Эта форма содержит элементы управления listbox и button. Оба элемента используют имена по умолчанию listBox1 и button1:

namespace SampleBase {

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

Затем включается пространство имен для ссылки на msxml3.dll. Помните, что ссылку на эту библиотеку необходимо включать в проект (ее можно найти на вкладке COM диалогового окна Add Reference).

using MSXML2;

/// <summary>

/// Краткое описание Form1.

/// </summary>

public class Form1 : System.Windows.Forms.Form {

private System.Windows.Forms.ListBox listBox1;

private System.Windows.Forms.Button button1;

/// <summary>

/// Необходимая для Designer переменная.

/// </summary>

private System.ComponentModel.Container components;

Затем объявляется документ DOM на уровне модуля:

private DOMDocument30 doc;

public Form1() {

//

// Требуется для поддержки Windows Form Designer

//

InitializeComponent();

//

// TODO: Добавьте любой код конструктора после вызова

// InitializeComponent

//

}

/// <summary>

/// Очистить все использованные ресурсы.

/// </summary>

public override void Disposed {

base.Dispose();

if (components != null) components.Dispose();

}

#region Windows Form Designer создает код

/// <summary>

/// Необходимый для поддержки Designer метод — не изменяйте

/// содержимое этого метода редактором кода.

/// </summary>

private void InitializeComponent() {

this.listBox1 = new System.Windows.Forms.ListBox();

this.button1 = new System.Windows.Forms.Button();

this.listBox1.Anchor = ((System.Windows.Forms.AnchorStyles.Top |

System.Windows.Forms.AnchorStyles.Left) |

System.Windows.Forms.AnchorStyles.Right);

this.listBox1.Size = new System.Drawing.Size(336, 238);

this.listBox1.TabIndex = 0;

this.listBox1.SelectedIndexChanged += new System.EventHandler(this.listBox1_SelectedIndexChanged);

this.button1.Anchor = System.Windows.Forms.AnchorStyles.Bottom;

this.button1.Location = new System.Drawing.Point(136, 264);

this.button1.TabIndex = 1;

this.button1.Text = "button1";

this.button1.Click += new System.EventHandler(this.button1_Click);

this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);

this.ClientSize = new System.Drawing.Size(339, 320);

this.Controls.AddRange(new System.Windows.Forms.Control[]{this.button1, this.listBox1});

this.Text = "Form1";

}

#endregion

/// <summary>

/// Главная точка входа для приложения.

/// </summary>

[STAThread]

static void Main() {

Application.Run(new Form1());

}

Мы хотим взять номер ISBN из listbox и, используя простой поиск XPath, найти узел книги, который ему соответствует, и вывести текст узла (заглавие книги и цену) в MessageBox. Язык пути доступа XML (XPath) является нотацией XML, которая может использоваться для запроса и фильтрации текста в документе XML. Рассмотрим XPath в .NET позже в этой главе. Вот код обработчика событий для выбора записи в окне списка:

protected void listBox1_SelectedIndexChanged (object sender, System.EventArgs e){

string srch=listBox1.SelectedItem.ToString();

IXMLDOMNode nd=doc.selectSingleNode("bookstore/book[@ISBN='" + srch + "']");

MessageBox.Show(nd.text);

}

Теперь мы имеем обработчик события нажатия кнопки. Сначала мы загружаем файл books.xml — обратите внимание, что если файл выполняется не в папке bin/debug или bin/release, необходимо исправить соответствующим образом путь доступа:

protected void button1_Click(object sender, System.EventArgs e) {

doc=new DOMDocument30();

doc.load("..\\..\\..\\books.xml")

Следующие строки объявляют, что узлы являются nodeList узлов книг. В данном случае имеется три узла:

IXMLDOMNodeList nodes;

nodes = doc.selectNodes("bookstore/book");

IXMLDOMNode node=nodes.nextNode();

Мы просматриваем узлы в цикле и добавляем текстовое значение атрибута ISBN в listBox1:

while(node!=null) {

listBox1.Items.Add(node.attributes.getNamedItem("ISBN").text);

node=nodes.nextNode();

}

}

}

}

Вот как выглядит пример во время выполнения:

Это изображение появляется после того, как была нажата кнопка button1 и загрузился listBox1 с номерами ISBN книг. После выбора номера ISBN будет выведено следующее:

System.Xml

Пространство имен System.Xml является мощным и относительно простым для использования, но оно отличается от модели MSXML 3.0. Если вы знакомы с MSXML 3.0, то применяйте его, пока не освоитесь с пространством имен System.Xml. Пространство имен System.Xml предлагает большую гибкость и легче расширяется.

Этот файл XML будет использоваться в примерах этой главы. Код, который только что был рассмотрен, лежит в основе нескольких примеров. В большинстве других будет показан только код, имеющий отношение к делу, и не будет повторяться то, что уже было показано.

Чтение и запись XML

Теперь посмотрим, что позволяет делать платформа .NET. Если раньше вы работали с SAX, то XmlReader и XmlWriter вам будут знакомы. Классы на основе XmlReader предоставляют быстрый курсор только для чтения вперед, который создает поток данных XML для обработки. Так как это потоковая модель, то требования к памяти не очень большие. Однако в ней отсутствует навигационная гибкость и средства чтения/записи, присущие модели DOM. Классы на основе XmlWriter будут создавать документ XML, который соответствует рекомендациям по пространствам имен XML 1.0 консорциума W3C.

XmlReader и XmlWriter являются абстрактными классами. Рисунок ниже показывает, какие классы являются производными от XmlReader и XmlWriter:

XmlTextReader и XmlTextWriter работают либо с объектами на основе потока, либо с объектами на основе TextReader или TextWriter. XmlNodeReader использует XmlNode вместо потока в качестве своего источника. XmlValidatingReader добавляет DTD и проверку схем и поэтому предлагает проверку данных. Мы рассмотрим это подробнее позже в этой главе.

XmlTextReader

XmlTextReader похож на SAX. Одно из различий заключается в том, что SAX является моделью типа рассылки (push), т.е. посылает данные приложению и разработчик должен быть готов принять их, a XmlTextReader применяет модель запроса (pull), где данные посылаются приложению, которое их запрашивает. Это предоставляет более простую и интуитивно понятную модель для программирования. Другое преимущество состоит в том, что модель запроса может быть избирательной в отношении данных, посылаемых приложению. Если нужны не все данные, то их не нужно обрабатывать. В модели рассылки все данные XML должны быть обработаны приложением, нужны они ему или нет.

Возьмем простой пример считывания данных XML, и затем более внимательно рассмотрим класс XmlTextReader. Код можно найти в папке XmlReaderSample1. Можно заменить метод button1_Click в предыдущем примере на следующий код. Эту версию данного кода можно найти в папке SampleBase2 загруженного архива кода. Не забудьте изменить:

using MSXML2;

на

using System.Xml;

Мы должны это сделать, поскольку используем теперь не MSXML 3.0, а пространство имен System.Xml. Нужно также удалить метод listBox1_SelectedIndexChanged, так как он включает в себя некоторые неподдерживаемые методы и строку:

private DOMDocument30 doc;

protected void button1_Click(object sender, System.EventArgs e) {

// Измените этот путь доступа, чтобы найти books.xml

string fileName = "..\\..\\..\\books.xml";

// Создать новый объект TextReader

XmlTextReader tr = new XmlTextReader(fileName);

// Прочитать узел за раз

while(tr.Read()) {

if (tr.NodeType == XmlNodeType.Text) listBox1.Items.Add(tr.Value);

}

}

Это XmlTextReader в простейшей форме. Сначала создается строковый объект fileName с именем файла XML. Затем создается новый объект XmlTextReader, передавая в качестве параметра строку fileName.XmlTextReader в настоящее время имеет 13 различных перегружаемых конструкторов, которые получают различные комбинации строк (имен файлов и URL), потоков и таблиц имен. После инициализации объекта XmlTextReader ни один узел не выбран. Это единственный момент, когда узел не является текущим. Когда мы начинаем цикл tr.Read, первая операция чтения Read переместит нас в первый узел документа. Обычно это бывает узел Declaration XML. В этом примере при переходе к каждому узлу tr.NodeType сравнивается с перечислением XmlNodeType, и когда встречается текстовый узел, значение текста добавляется в listbox. Вот экран после того, как было загружено окно списка:

Существует несколько способов перемещения по документу. Как мы только что видели, Read перемещает нас к следующему узлу. Затем можно проверить, имеет ли узел значение (HasValue) или, как мы скоро увидим, имеет ли узел атрибуты (HasAttributes). Существует метод ReadStartElement, который проверяет, является ли текущий узел начальным элементом, и затем перемешает текущую позицию к следующему узлу. Если текущая позиция не является начальным элементом, то порождается исключение XmlException. Этот метод совпадает с вызовом метода IsStartElement, за которым следует метод Read.

Методы ReadString и ReadCharts считывают текстовые данные из элемента. ReadString возвращает строковый объект, содержащий данные, в то время как ReadCharts считывает данные в заданный массив символов.

Метод ReadElementString аналогичен методу ReadString, за исключением того, что при желании можно передать в него имена элемента. Если следующий узел содержимого не является начальным тегом или, если параметр Name не совпадает с именем (Name) текущего узла, то порождается исключение. Вот пример того, как это может использоваться (код можно найти в папке XmlReaderSample2):

protected void button1_Click(object sender, System.EventArgs e) {

// Использовать файловый поток для получения данных

FileStream fs = new FileStream("..\\..\\..\\books.xml", FileMode.Open);

XmlTextReader tr = new XmlTextReader(fs);

while(!tr.EOF) {

// если встретился тип элемента, проверить и загрузить его в окно списка

if (tr.MoveToContent()==XmlNodeType.Element && tr.Name=="title") {

listBox1.Items.Add(tr.ReadElementString());

} else

//иначе двигаться дальше

tr.Read();

}

}

В цикле while используется метод MoveToContent для поиска каждого узла типа XmlNodeType.Element с именем title. Если это условие не выполняется, то предложение else вызывает метод Read для перехода к следующему узлу. Если будет найден узел, соответствующий критерию, то результат работы метода ReadElementString добавляется в listbox. Таким образом мы получим заглавия книг в listbox. Отметим, что после успешного применения ReadElementString метод Read не вызывается. Это связано с тем, что метод ReadElementString обрабатывает весь Element и перемещается к следующему узлу.

Если удалить && tr.Name=="title" из предложения if, то придется отслеживать исключение XmlException, когда оно будет порождаться. При просмотре файла данных можно заметить, что первым элементом, который найдет метод MoveToContent, является элемент <bookstore>. Как элемент он будет проходить проверку в операторе if. Но так как он не содержит простой текстовый тип, он вынуждает метод ReadElementString порождать исключение XmlException. Одним из способов обхода этой проблемы является размещение вызова ReadElementString в своей собственной функции. Назовем ее LoadList. XmlTextReader передается в нее в качестве параметра. Теперь, если вызов ReadElementString отказывает внутри этой функции, мы можем иметь дело с ошибкой и вернуться назад в вызывающую функцию. Вот как выглядит пример с этими изменениями (код можно найти в папке XmlReaderSample3):

protected void button1_Click(object sender, System.EventArgs e) {

// использовать файловый поток для получения данных

FileStream fs = new FileStream("..\\..\\..\\books.xml", FileMode.Open);

XmlTextReader tr = new XmlTextReader(fs);

while(!tr.EOF) {

// если встретился тип элемента, проверить и загрузить его в окно списка

if (tr.MoveToContent() == XmlNodeType.Element) {

LoadList(tr);

} else

// иначе двигаться дальше

tr.Read();

}

}

private void LoadList(XmlReader reader) {

try {

listBox1.Items.Add(reader.ReadElementString());

}

//если инициировано исключение XmlException, игнорировать его.

catch(XmlException er){}

}

Вот что должно появиться, когда код будет выполнен:

Это тот же результат, который был раньше. Мы видим, что существует более одного способа достичь одной и той же цели. При этом становится очевидной гибкость пространства имен System.Xml.

По мере чтения узлов можно заметить отсутствие каких-либо атрибутов. Это связано с тем, что атрибуты не считаются частью структуры документа. При нахождении в узле элемента мы можем проверить наличие атрибутов и получить значения атрибутов. Метод HasAttributes возвращает true, если существуют какие-либо атрибуты, иначе возвращается false. Свойство AttributeCount сообщит, сколько имеется атрибутов. Метод GetAttribute получает атрибут по имени или по индексу. Если желательно просмотреть все атрибуты по очереди, можно использовать методы MoveToFirstAttribute (перейти к первому атрибуту) и MoveToNextAttribute (перейти к следующему атрибуту). Вот пример просмотра атрибутов из XmlReaderSample4:

protected void button1_Click(object sender, System.EventArgs e) {

// задаем путь доступа в соответствии со структурой путей доступа

// к данным

string fileName = "..\\..\\..\\books.xml";

// Создать новый объект TextReader

XmlTextReader tr = new XmlTextReader(filename);

// Прочитать узел за раз

while (tr.Read()) {

// проверить, что это элемент NodeType

if (tr.NodeType = XmlNodeType.Element) {

// если это — элемент, то посмотрим атрибуты

for(int i=0; i<tr.AttributeCount; i++) {

listBox1.Items.Add(tr.GetAttribute(i));

}

}

}

}

На этот раз мы ищем узлы элементов. Когда такой узел найден, в цикле просматриваются все атрибуты и с помощью метода GetAttribute значение атрибута загружается в listbox.

Проверка

Если нужно проверить документ XML, используйте класс XmlValidatingReader. Он обладает всей функциональностью класса XmlTextReader (оба реализуют XmlReader, но XmlValidatingReader добавляет свойство ValidationType, свойство Schemes и свойство SchemaType). Свойство ValidationType задается как тип проверки, которую желательно выполнить. Допустимые значения этого свойства следующие:

Значение свойства

Описание

Auto

Если в <!DOCTYPE...> объявлен DTD, он и будет загружаться и обрабатываться. Атрибуты по умолчанию и общие сущности, определенные в DTD, станут доступными. Если найден атрибут XSD schemalocation, то загружается и обрабатывается XSD, при этом все атрибуты по умолчанию, определенные в схеме, будут возвращены. Если найдено пространство имен с префиксом MSXML x-schema:, загрузится и обработается схема XDR, все атрибуты, определенные по умолчанию, возвратятся.

DTD

Проверка согласно правилам DTD.

Schema

Проверка согласно правилам XSD.

XDR

Проверка согласно правилам XDR.

None

Проверка не выполняется.

Если свойство задано, то должен быть назначен обработчик событий ValidationEventHandler. Событие инициируется, когда случается ошибка проверки. На ошибку можно отреагировать любым подходящим образом. Рассмотрим пример. Добавим пространство имен схемы XDR (XML Data Reduced — приведенные данные XML) к файлу books.xml и назовем этот файл booksVal.xml. Теперь он выглядит так:

<?xml version='1.0'?>

<!-- Этот файл представляет фрагмент базы данных учета запасов книжного склада -->

<bookstore xmlns="x-schema:books.xdr">

<book genre="autobiography" publicationdate="1981" ISBN="1-861003-11-0">

<title>The Autobiography of Benjamin Franklin</title>

<author>

<first-name>Benjamin</first-name>

<last-name>Franklin</last-name>

</author>

<price>8.99</price>

</book>

<book genre="novel" publicationdate="1967" ISBN="0-201-63361-2">

<title>The Confidence Man</title>

<author>

<first-name>Herman</first-name>

<last-name>Melville</last-name>

</author>

<price>11.99</price>

</book>

<book genre="philosophy" publicationdate="1991" ISBN="1-861001-57-6">

<title>The Gorgias</title>

<author>

<name>Plato</name>

</author>

<price>9.99</price>

</book>

</bookstore>

Отметим, что элемент bookstore имеет теперь атрибут xmlns="x-schema:books.xdr". Это будет указывать на следующую схему XDR:

<?xml version="1.0"?>

<Schema xmlns="urn:schemas-microsoft-com:xml-data" xmlns:dt="urn:schemas-microsoft-com:datatypes">

<ElementType name="first-name" content="textOnly"/>

<ElementType name="last-name" content="textOnly"/>

<ElementType name="name" content="textOnly"/>

<ElementType name="price" content="textOnly" dt:type="fixed.14.4"/>

<ElementType name="author" content="eltOnly" order="one">

<group order="seq">

<element type="name"/>

</group>

<group order="seq">

<element type="first-name"/>

<element type="last-name"/>

</group>

</ElementType>

<ElementType name="title" content="textOnlу" />

<AttributeType name="genre" dt:type="string"/>

<ElementType name="book" content="eltOnly">

<attribute type="genre" required="yes"/>

<element type="title"/>

<element type="author"/>

<element type="price"/>

</ЕlementType>

<ElementType name="bookstore" content="eltOnly">

<element type="book"/>

</ElementType>

</Schema>

Отметим, что имеются два атрибута в файле XML, которые не определены в схеме. Если посмотреть внимательно, то можно увидеть что в схеме нет атрибутов publication-date и ISBN из элемента book. Мы сделали это, чтобы показать, что проверка действительно выполняется. Можно использовать для подтверждения этого следующий код. Необходимо будет добавить в класс using System.Xml.Schema. Весь код доступен в XMLReaderSample5:

protected void button1_Click (object sender, System.EventArgs e) {

//измените это в соответствии с используемой структурой путей доступа.

string filename = "..\\..\\..\\booksVal.xml";

XmlTextReader tr = new XmlTextReader(filename);

XmlValidatingReader trv=new XmlValidatingReader(tr);

// Задать тип проверки

trv.ValidationType=ValidationType.xdr;

// Добавить обработчик события проверки

trv.ValidationEventHandler += new ValidationEventHandler(this.ValidationEvent);

// Считываем узел за раз

while(trv.Read()) {

if (trv.NodeType == XmlNodeType.Text) listBox1.Items.Add(trv.Value);

}

}

public void ValidationEvent(object sender, ValidationEventArgs args) {

MessageBox.Show(args.Message);

}

Мы создаем XmlTextReader для передачи в XmlValidationReader. Когда XmlValidationReader trv создан, можно использовать его по большей части так же, как XmlTextReader в предыдущих примерах. Различия состоят в том что в данном случае определен атрибут ValidationType и добавлен ValidationEventHandler. Каждый раз при возникновении ошибки проверки инициируется ValidationEvent. Затем можно будет обработать ошибку проверки любым приемлемым способом. В данном примере выводится MessageBox с описанием ошибки. Вот как выглядит MessageBox, когда инициируется ValdationEvent.

В отличие от некоторых синтаксических анализаторов XmlValidationReader после возникновения ошибки продолжает считывание. Имеется возможность определить серьезность ошибки проверки. Если окажется, что это серьезная ошибка, то можно остановить чтение.

Свойство Schemas класса XmlValidationReader содержит коллекцию XmlSchemaCollection, которая находится в пространстве имен System.Xml.Schema. В этой коллекции находятся предварительно загруженные схемы XSD и XDR, что позволяет выполнить очень быструю проверку, (особенно, если нужно проверить несколько документов), так как схему не нужно каждый раз перезагружать. Для получения выигрыша в производительности и создается объект XmlSchemaCollection. Метод Add имеет четыре перегружаемые версии. Можно передать объект на основе XmlSchema, объект на основе XmlSchemaCollection, строку string с пространством имен вместе со строкой string с URL файла схемы и, наконец, строку string с пространством имен и объектом на основе XmlReader, который содержит схему.

Запись XML

Класс XmlTextWriter позволяет записывать XML в поток, файл или объект TextWriter. Подобно XmlTextReader он делает это только вперед, некэшируемым образом. XmlTextWriter можно конфигурировать различным образом, что позволяет определить такие вещи, как наличие или отсутствие отступов, величину отступа, какой использовать символ кавычки в значениях атрибутов, и поддерживаются ли пространства имен. Свойство DataTypeNamespace определяет, как строго значения типов преобразуются в текст XML. Для этого свойства допустимо значение urn:schemas-microsoft-com:datatypes, которое поддерживает типы данных XDR, и другое значение www.w3.org/1999/XMLSchema-data-types, которое является схемой W3C типов данных XSD. Чтобы использовать, например, тип данных TimeSpan, необходимо будет задать это свойство для типов данных XSD.

Приведем простой пример, чтобы увидеть, как может использоваться класс TextWriter(пример находится в папке XMLWriterSample1):

private void button1_Click(object sender, System.EventArgs e) {

// измените в соответствии с используемой структурой путей доступа

string fileName="..\\..\\..\\booknew.xml";

//создайте XmlTextWriter

XmlTextWriter tw=new XmlTextWriter(fileName, null);

// задайте форматирование с отступом

tw.Formatting=Formatting.Indented;

tw.WriteStartDocument();

//Начать создание элементов и атрибутов

tw.WriteStartElement("book");

tw.WriteAttributeString("genre", "Mystery");

tw.WriteAttributeString("publicationdate", "2001");

tw.WriteAttributeString("ISBN", "123456789");

tw.WriteElementString("title", "Case of the Missing Cookie");

tw.WriteStartElement("author");

tw.WriteElementString("name", "Cookie Monster");

tw.WriteEndElement();

tw.WriteElementString("price", "9.99");

tw.WriteEndElement();

tw.WriteEndDocument();

// очистить

tw.Flush();

tw.Close();

}

Создадим новый файл booknew.xml и добавим новую книгу. Объект XmlTextWriter заменит существующий файл. Вставку нового элемента или узла в существующий документ рассмотрим позже. Экземпляр объекта XmlTextWriter создается с помощью объекта FileStream в качестве параметра. Можно также передать строку с именем файла и путем доступа или объект на основе TextWriter. При задании свойства Indenting узлы-потомки будут автоматически делать отступ от предка. Метод WriteStartDocument() помещает объявление документа. Начинаем запись данных. Сначала идет элемент book. Затем добавляем атрибуты genre, publicationdate и ISBN. После чего записываем элементы title, author, и price. Отметим, что элемент author имеет элемент-потомок name.

После нажатия на кнопку будет создан следующий файл booknew.xml:

<?xml version="1 .0"?>

<book genre= "Mystery" publicationdate="2001" ISBN="123456789">

<title>Case of the Missing Cookie</title>

<author>

<name>Cookie Monster</name>

</author>

<price>9,99</price>

</book>

Так же как в документе XML, здесь имеются начальный метод и конечный метод (WriteStartElement и WriteEndElement). Вложенность контролируется отслеживанием начала и окончания записи элементов и атрибутов. Это можно видеть при добавлении элемента потомка name к элементу authors. Отметим, как организуются вызовы методов WriteStartElement и WriteEndElement и как это связывается с выведенным документом XML.

В дополнение к WriteElementString и WriteAtributeString имеется несколько других специализированных методов записи. Метод WriteCDate будет выводить раздел CDate (<!CDATE[...]]>), взяв текст для записи из параметра. WriteComment записывает комментарий в подходящем формате XML. WriteChars записывает содержимое символьного буфера. Это работает аналогично методу ReadChars, который был рассмотрен ранее. Оба они используют один и тот же тип параметров. Методу WriteChar нужен буфер (массив символов), начальная позиция для записи (целое значение) и число символов для записи (целое значение).

Чтение и запись XML с помощью классов, основанных на XMLReader и XMLWriter, осуществляются очень просто. Далее мы рассмотрим реализацию DOM пространства имен System.Xml. Это классы на основе XmlDocument и XmlNode.

Объектная модель документа в .NET

Реализация объектной модели документа (DOM, Document Object Model) в .NET поддерживает спецификации W3C DOM Level 1 и Core DOM Level 2. DOM реализуется с помощью класса XmlNode. XmlNode является абстрактным классом, который представляет узел документа XML. XmlNodeList является упорядоченным списком узлов. Это живой список узлов, и любые изменения в любом узле немедленно отражаются в списке. XmlNodeList поддерживает индексный доступ или итеративный доступ. Эти два класса составляют основу реализации DOM на платформе .NET. Вот список классов, которые основываются на XmlNode.

Имя класса

Описание

XmlLinkedNode

Расширяет XmlNode. Возвращает узел непосредственно перед или после текущего узла. Добавляет свойства NextSibling и PreviousSibling в XmlNode.

XmlDocument

Расширяет XmlNode. Представляет весь документ. Реализует спецификации DOM Level 1 и Level 2.

XmlAttribute

Расширяет XmlNode. Объект атрибута объекта XmlElement.

XmlCDataSection

Расширяет XmlCharacterData. Объект, который представляет раздел документа CData.

XmlCharacterData

Абстрактный класс, который предоставляет методы манипуляции с текстом для других классов. Расширяет XmlLinkedNode.

XmlComment

Расширяет XmlCharacterData. Представляет объект комментария XML.

XmlDeclaration

Расширяет XmlLinkedNode. Представляет узел объявления (<?xml version='1.0' ...>)

XmlDocumentFragment

Расширяет XmlNode. Представляет фрагмент дерева документа.

XmlDocumentType

Расширяет XmlLinkedNode. Данные, связанные с объявлением типа документа.

XmlElement

Расширяет XmlLinkedNode. Объект элемента XML.

XmlEntity

Расширяет XmlNode. Синтаксически разобранный или неразобранный узел сущности.

XmlEntityReferenceNode

Расширяет XmlLinkedNode. Представляет ссылочный узел сущности

XmlNotation

Расширяет XmlNode. Содержит нотацию, объявленную в DTD или в схеме.

XmlProcessingInstruction

Расширяет XmlLinkedNode. Содержит инструкцию обработки XML.

XmlSignificantWhitespace

Расширяет XmlCharacterData. Представляет узел с разделителем. Узлы создаются, только если флаг PreserveWhiteSpace задан как true.

XmlWhitespace

Расширяет XmlCharacterData. Представляет разделитель в содержимом элемента. Узлы создаются, только если флаг PreserveWhiteSpace задан как true.

XmlText

Расширяет XmlCharacterData. Текстовое содержимое элемента или атрибута.

Как можно видеть .NET делает доступным класс, соответствующий почти любому типу XML. Мы не будем рассматривать каждый класс подробно, но разберем несколько примеров. Вот как выглядит диаграмма наследования:

Первый пример будет создавать объект XmlDocument, загружать документ с диска и загружать окно списка с данными из элементов title. Это аналогично одному из примеров, которые были выполнены в разделе XmlReader. Отличие заключается в том, что осуществляется выбор, с какими узлами мы хотим работать, вместо того чтобы использовать весь документ. Вот код для выполнения этого в среде XmlNode. Посмотрите, как просто он выглядит при сравнении (файл можно найти в папке DOMSample1 загруженного архива):

private void button1_Click(object sender. System.EventArgs e) {

// doc объявлен на уровне модуля

// изменить путь доступа в соответствии со структурой путей доступа

doc.Load("..\\..\\..\\books.xml")

// получить только те узлы, которые нужны

XmlNodeList nodeLst=doc.GetElementsByTagName("title");

// итерации по списку XmlNodeList

foreach(XmlNode node in nodeLst) listBox1.Items.Add(node, InnerText);

}

Обратите внимание, что мы добавили следующее объявление на уровне модуля:

private XmlDocument doc=new XmlDocument();

Если бы это было все, что нужно делать, то использование XmlReader было бы значительно более эффективным способом загрузки окна списка. Причина в том, что мы прошли через документ один раз и затем закончили с ним работу. Однако, если желательно повторно посетить узел, то использование XmlDocument является лучшим для этого способом. Слегка расширим пример (новая версия находится в DOMSample2):

private void button1_Click(object sender, System.EventArgs e) {

//doc объявлен на уровне модуля

// измените путь доступа в соответствии со структурой путей доступа

doc.Load("..\\..\\..\\books.xml");

// получить только те узлы, которые хотим XmlNodeList

nodeLst=doc.GetElementsByTagName("title");

// итерации через список XmlNodeList

foreach(XmlNode node in nodeLst) listBox1.Items.Add(node.InnerText);

}

private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e) {

// создать строку поиска XPath

string srch="bookstore/book[title='" + listBox1.SelectedItem.ToString() + "']";

// поиск дополнительных данных

XmlNode foundNode=doc.SelectSingleNode(srch);

if (foundNode!=null) MessageBox.Show(foundNode.InnerText);

else MessageBox.Show("Not found");

}

В этом примере listbox с заголовками загружается из документа books.xml. Когда мы щелкаем на окне списка, вызывая порождение события SelectedIndexChange (не забудьте добавить код, присоединяющий обработчик событий в функцию InitializeComponent), мы берем текст выбранного пункта в listbox, в данном случае заголовок книги, создаем оператор XPath и передаем его в метод SelectSingleNode объекта doc. Он возвращает элемент book, частью которого является title (foundNode). Выведем для наглядности InnerText узла в окне сообщения. Мы можем продолжать щелкать на элементах в listbox сколько угодно раз, так как документ загружен и остается загруженным, пока мы его не освободим.

Небольшой комментарий в отношении метода SelectSingleNode. Это реализация XPath в классе XmlDocument. Существуют методы SelectSingleNode и SelectNodes. Оба они определены в XmlNode, на котором основывается XmlDocument. SelectSingleNode возвращает XmlNode, и SelectNodes возвращает XmlNodeList. Пространство имен System.Xml.XPath содержит более насыщенную реализацию XPath (см. ниже).

Ранее рассматривался пример XmlTextWriter, который создает новый документ. Ограничение состояло в том, что он не вставлял узел в текущий документ. Это можно сделать с помощью класса XmlDocument. Если изменить button1_Click из предыдущего примера, то получим следующий код (DOMSample3):

private void button1_Click(object sender, System.EventArgs e) {

// изменить путь доступа, как требуется существующей структурой

doc.Load("..\\..\\..\\books.xml");

// создать новый элемент 'book'

XmlElement newBook=doc.CreateElement("book");

// задать некоторые атрибуты

newBook.SetAttribute("genre", "Mystery");

newBook.SetAttribute("publicationdate", "2001");

newBook.SetAttricute("ISBN", "123456789");

// создать новый элемент 'title'

XmlElement newTitle=doc.CreateElement("title");

newTitle.InnerText="Case of the Missing cookie";

newBook.AppendChild(newTitle);

// создать новый элемент author

XmlElement newAuthor=doc.CreateElement("author");

newBook.AppendChild(newAuthor);

// создать новый элемент name

XmlElement newName=doc.CreateElement("name");

newName.InnerText="С. Monster";

newAuthor.AppendChild(newName);

// создать новый элемент price

XmlElement newPrice=doc.CreateElement("price");

newPrice.innerText="9.95";

newBook.AppendChild(newPrice);

// добавить к текущему документу

doc.DocumenElement.AppendChild(newBook);

// записать doc на диск

XmlTextWriter tr=new XmlTextWriter("..\\..\\..\\booksEdit.xml", null);

tr.Formatting=Formatting.Indented;

doc.WriteContentTo(tr);

tr.Close();

// загрузить listBox1 со всеми заголовками, включая новый

XmlNodeList nodeLst=doc.GetElementsByTagName("title");

foreach(XmlNode node in nodeLst) listBox1.Items.Add(node.InnerText);

}

private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e) {

string srch="bookstore/book[title='" + listBox1.SelectedItem.ToString() + "']";

XmlNode foundNode=doc.SelectSingleNode(srch);

if (foundNode!=null) MessageBox.Show(foundNode.InnerText);

else MessageBox.Show("Not found");

}

При выполнении этого кода будет получена функциональность предыдущего примера, но в окне списка появилась одна дополнительная книга "The Case of Missing Cookie". Щелчок мыши на заголовке этой книги приведет к выводу такой же информации, как и для других книг. Анализируя код, можно увидеть, что это достаточно простой процесс. Прежде всего создается новый элемент book:

XmlElement newBook = doc.CreateElement("book);

Метод CreateElement имеет три перегружаемые версии, которые позволяют определить имя элемента, имя и пространство имен URI, и, наконец, prefix (префикс), lоcalname (локальное имя) и namespace (пространство имен). Когда элемент создан, необходимо добавить атрибуты

newBook.setAttribute("genre", "Mystery");

newBook.SetAttribute("publicationdate", "2001");

newBook.SetAttribute("ISBN", "123456789");

Напомним, что класс XmlAttribute расширяет класс XmlNode, поэтому нам доступны все свойства и методы XmlNode. Даже если имеется очень сложная структура, то при ее размещении никаких проблем возникать не должно.

Теперь, когда атрибуты созданы и необходимо добавить другие элементы книги:

XmlElement newTitle=doc.CreateElement("title");

newTitle.InnerText="Case of the Missing Cookie";

newBook.AppendChild(newTitle);

Здесь снова создается новый объект на основе XmlElement (newTitle). Присваиваем свойству InnerText заголовок новой книги и добавляем потомок к элементу book. Затем это повторяется для остальных элементов book. Отметим, что элемент name добавлен как потомок элемента author. Это дает нам правильное отношение вложенности.

Наконец, мы добавляем элемент newBook к узлу doc.DocumentElement. Это тот же уровень, что и у всех других элементов book. Мы заменили существующий документ новым, в отличие от XmlWriter, где можно было только создать новый документ. Последнее, что нужно сделать, это записать новый документ XML на диск. В этом примере мы создаем новый XmlTextWriter и передаем его в метод WriteContentTo. Не забудьте вызвать метод Close на XmlTextWriter, чтобы сбросить содержимое внутренних буферов и закрыть файл. Методы WriteContentTo и WriteTo получают XmlTextWriter в качестве параметра. WriteContentTo сохраняет текущий узел и всех потомков в XmlTextWriter, в то время как WriteTo сохраняет текущий узел. Так как doc является объектом на основе XmlDocument, он представляет весь документ и поэтому будет сохранен. Можно было бы также использовать метод Save. Он всегда будет сохранять весь документ. Save имеет четыре перегружаемые версии. Можно определить строку с именем файла и путем доступа, объект на основе класса Stream, объект на основе класса TextWriter, или объект на основе XmlWriter. Именно это было использовано при выполнении примера. Отметим новую запись в конце списка:

Если нужно создать документ с самого начала, можно использовать класс XmlTextWriter. Можно также использовать XmlDocument. Какой из них выбрать? Если данные, которые желательно поместить в XML, доступны и готовы для записи, то самым подходящий будет класс XmlTextWriter. Однако, если необходимо создавать документ XML постепенно, вставляя узлы в различные места, то наиболее приемлемым будет применение XmlDocument. Вот тот же пример, который только что был рассмотрен, но вместо редактирования текущего документа мы создаем новый документ (DOMSample4):

private void button1_Click(object sender, System.EventArgs e) {

// создать раздел объявлений

XmlDeclaration newDoc=doc.CreateXmlDeclaration("1.0", null, null);

doc.AppendChild(newDoc);

// создать новый корневой элемент

XmlElement newRoot=doc.CreateElement("newBookstore");

doc.AppendChild(newRoot);

// создать новый элемент 'book'

XmlElement newBook=doc.CreateElement("book");

// создать и задать атрибуты элемента "book"

newBook.SetAttribute("genre","Mystery");

newBook.SetAttribute("publicationdate", "2001");

newBook.SetAttribute("ISBN", "123456789");

// создать элемент 'title'

XmlElement newTitle=doc.CreateElement("title");

newTitle.InnerText="Case of the Missing Cookie";

newBook.AppendChild(newTitle);

// создать элемент author

XmlElement newAuthor=doc.CreateElement("author");

newBook.AppendChild(newAuthor);

// создать элемент name

XmlElement newName=doc.CreateElement("name");

newName.InnerText="C. Monster";

newAuthor.AppendChild(newName);

// создать элемент price

XmlElement newPrice=doc.CreateElement("price");

newPrice.InnerText="9.95";

newBook.AppendChild(newPrice);

// добавить элемент 'book' к doc

doc.DocumentElement.AppendChild(newBook);

// записать на диск Note новое имя файла booksEdit.xml

XmlTextWriter tr=new XmlTextWriter("..\\..\\..\\booksEdit.xml", null);

tr.Formatting=Formatting.Indented; doc.WriteContentTo(tr);

tr.Close();

// загрузить заголовок в окно списка

XmlNodeList nodeLst=doc.GetElementsByTagName("title");

foreach(XmlNode node in nodeLst) listBox1.Items.Add(node.InnerText);

}

private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e) {

String srch="newBookstore/book[title='"+ listBox1.SelectedItem.ToString() + "']";

XmlNode foundNode=doc.SelectSingleNode(srch);

if (foundNode!=null) MessageBox.Show(foundNode.InnerText);

else MessageBox.Show("Not found");

}

Заметим, что изменились только две начальные строки. Прежде чем сделать doc.Load, внесем новые элементы:

XmlDeclaration newDoc=doc.CreateXmlDeclaration("1.0", null, null);

doc.AppendChild(newDoc);

XmlElement newRoot=doc.CreateElement("newBookstore");

doc.AppendChild(newRoot);

Сначала создается новый объект XmlDeclaration. Параметрами являются версия (в настоящее время всегда "1.0"), кодировка (null подразумевает UTF-8) и, наконец, флаг standalone. Он может быть yes или no, но если вводится null или пустая строка, как в нашем случае, этот атрибут не будет добавляться при сохранении документа. Параметр кодировки должен задаваться строкой, которая является частью класса System.Text.Encoding, если не используется null.

Следующим создаваемым элементом станет DocumentElement. В данном случае мы называем его newBookstore, чтобы можно было видеть различие. Остальная часть кода является такой же, как и в предыдущем примере, и работает точно так же. Вот файл booksEdit.xml, создаваемый этим кодом:

<?xml version="1.0"?>

<newBookstore>

<book genre="Mystery" publicationdate="2001" ISBN="123456789">

<title>Case of the Missing Cookie</title>

<author>

<name>C. Monster</name>

</author>

<price>9.95</price>

</book>

</newBookstore>

Мы не рассмотрели всех особенностей класса XmlDocument или других классов, способствующих созданию модели DOM в .NET. Однако мы видели мощь и гибкость, которые предлагает реализация DOM в .NET. Класс XmlDocument обычно используется, когда требуется случайный доступ к документу. Используйте классы на основе XmlReader, когда желательна модель потокового типа. Помните, что гибкость XmlDocument на основе XmlNode обеспечивается более высокими требованиями к памяти, поэтому подумайте тщательно о том, какой метод предпочтительнее в конкретной ситуации.

XPath и XslTransform

Мы рассмотрим XPath и XslTransform вместе, хотя они являются отдельными пространствами имен на платформе. XPath содержится в System.Xml.XPath, a XslTransform находится в System.Xml.Xsl. Причина совместного рассмотрения состоит в том, что XPath, в частности класс XPathNavigator, предоставляет ориентированный на производительность способ выполнения XSLTransform в .NET. Для начала рассмотрим XPath, а затем его использование в классах System.Xsl.

XPath

Пространство имен XPath создается для скорости. Оно позволяет только читать документы XML без возможностей редактирования. XPath создается для поверхностного выполнения быстрых итераций и выбора в документе XML. Функциональность XPath представляется классом XPathNavigator. Этот класс может использоваться вместо XmlDocument, XmlDataDocument и XPathDocument. Если требуются средства редактирования, то следует выбрать XmlDocument; при работе с ADO.NET будет использоваться класс XmlDataDocument (мы увидим его позже в этой главе). Если имеет значение скорость, то применяйте в качестве хранилища XPathDocument. Можно расширить XPathNavigator для таких вещей, как файловая система или реестр в качестве хранилища. В следующей таблице перечислены классы XPath с кратким описанием назначения каждого класса:

Имя класса

Описание

XPathDocument

Представление всего документа XML. Только для чтения.

XPathNavigator

Предоставляет навигационные возможности для XPathDocument.

XPathNodeIterator

Обеспечивает итерацию по множеству узлов. Является эквивалентом для множества узлов в Xpath.

XPathExpression

Компилированное выражение Xpath. Используется SelectNodes, SelectSingleNodes, Evaluate и Matches.

XPathException

Класс исключений XPath.

XPathDocument не предлагает никакой функциональности класса XmlDocument. Он имеет четыре перегружаемые версии, позволяющие открывать документ XML из файла или строки пути доступа, объекта TextReader, объекта XmlReader или объекта на основе Stream.

Загрузим документ books.xml и поработаем с ним, чтобы можно было понять, как действует навигация. Чтобы использовать эти примеры, необходимо добавить ссылки на пространства имен System.Xml.Xsl и System.Xml.XPath следующим образом:

using System.Xml.XPath;

using System.Xml.Xsl;

Для данного примера воспользуемся файлом bookspath.xml. Он аналогичен books.xml, за исключением того, что добавлены дополнительные книги. Вот код формы, который находится в папке XPathXSLSample1:

private void button1_Click(object sender, System.EventArgs e) {

// изменить в соответствии с используемой структурой путей доступа

XPathDocument doc=new XPathDocument("..\\..\\..\\booksxpath.xml");

// создать XPathNavigator

XPathNavigator nav=((IXPathNavigable)doc).CreateNavigator();

// создать XPathNodeIterator узлов книг

// который имеют значение атрибута genre, совпадающее с novel

XPathNodeIterator iter=nav.Select("/bookstore/book[@genre='novel']");

while(iter.MoveNext()) {

LoadBook(iter.Current);

}

}

private void LoadBook(XPathNavigator lstNav) {

// Нам передали XPathNavigator определенного узла book,

// мы выберем всех прямых потомков и

// загрузим окно списка с именами и значениями

XPathNodeIterator iterBook=lstNav.SelectDescendants(XPathNodeType.Element, false);

while(iterBook.MoveNext())

listBox1.Items.Add(iterBook.Current.Name + ": " + iterBook.Current.Value);

}

Здесь сначала создается XPathDocument, передавая строку файла и пути доступа документа, который будет открыт. В следующей строке кода создается XPathNavigator:

XPathNavigator nav=((IXPathNavigable)doc).CreateNavigator();

Отметим, что здесь происходит преобразование типа интерфейса IXPathNavigable в только что созданный XPathNavigator, что вызывает метод CreateNavigator. После создания объекта XPathNavigator можно начать навигацию в документе.

Этот пример показывает, как применяются методы Select для получения множества узлов, которые имеют novel в качестве значения атрибута genre. Затем мы используем цикл MoveNext() для итераций по всем novels в списке книг.

Для загрузки данных в listbox используется свойство XPathNodeIterator.Current. При этом создается новый объект XPathNavigator на основе узла, на который указывает XPathNodeIterator. В данном случае создается XPathNavigator для одного узла book (книги) в документе. LoadBook создает другой XPathNodeIterator, вызывая иной тип метода выбора — метод SelectDescendants. Это даст нам XPathNodeIterator всех узлов-потомков и потомков узлов-потомков узла book (книга), переданного в метод LoadBook. Мы делаем другой цикл MoveNext() на этом XPathNodeIterator и загружаем окно списка именами и значениями элементов.

XPathNavigator содержит все методы для перемещения и выбора элементов, которые могут понадобиться. Приведем некоторые из методов перемещения:

Имя метода

Описание

MoveTo

Получает в качестве параметра XPathNavigator. Делает текущей позицию, которая указана в XPathNavigator.

MoveToAttribute

Перемещает к именованному атрибуту. Получает имя атрибута и пространство имен как параметры.

MoveToFirstAttribute

Перемещает к первому атрибуту текущего элемента. Возвращает true, если выполняется успешно.

MoveToNextAttribute

Перемещает к следующему атрибуту текущего элемента. Возвращает true, если выполняется успешно.

MoveToFirst

Перемещает к первому sibling текущего узла. Возвращает true, если выполняется успешно, в противном случае возвращает false.

MoveToLast

Перемещает к последнему sibling текущего узла. Возвращает true, если выполняется успешно.

MoveToNext

Перемещает к следующему sibling текущего узла. Возвращает true, если выполняется успешно.

MoveToPrevious

Перемещает к предыдущему sibling текущего узла. Возвращает true, если выполняется успешно.

MoveToFirstChild

Перемещает к первому потомку текущего элемента. Возвращает true, если выполняется успешно.

MoveToId

Перемещает к элементу с идентификатором ID, предоставленным в виде параметра. Должна существовать схема документа и данные элемента типа ID.

MoveToParent

Перемещает к предку текущего узла. Возвращает true, если выполняется успешно.

MoveToRoot

Перемещает к корневому узлу документа.

Существует также несколько методов Select выбора подмножества узлов для работы. Все методы Select возвращают объект XPathNodeIterator. XPathNodeIterator можно считать эквивалентом NodeList или NodeSet в XPath. Этот объект имеет три свойства и два метода:

□ Clone — создает новую копию себя

□ Count — число узлов в объекте XPathNodeIterator

□ Current — возвращает XPathNavigator, указывающий на текущий узел

□ CurrentPosition — возвращает целое число, соответствующее текущей позиции

□ MoveNext — перемещает в следующий узел, соответствующий выражению Xpath, которое создало XPathNodeIterator

Можно использовать также существующие методы SelectAncestors и SelectChildren. Они возвращают XPathNodelterator. В то время, как Select получает выражение XPath в качестве параметра, другие методы выбора получают в качестве параметра XPathNodeType. В рассматриваемом примере мы выбираем все узлы XPathNodeType.Element.

Вот как выглядит экран после выполнения кода. Обратите внимание, что все перечисленные книги являются романами (novel).

Для добавления стоимости книг XPathNavigator содержит метод Evaluate. Evaluate имеет три перегружаемые версии. Первая из них содержит строку, которая является вызовом функции XPath. Вторая перегружаемая версия Evaluate использует в качестве параметра объект XPathExpression, третья — XPathExpression и XPathNodeIterator. Сделаем следующие изменения в примере (эту версию кода можно найти в XPathXSLSample2):

private void button1_Click(object sender, System.EventArgs e) {

//изменить в соответствии со структурой путей доступа

XPathDocument doc=new XPathDocument("..\\..\\..\\booksxpath.XML");

//создать XPathNavigator

XPathNavigator nav=((IXPathNavigable)doc).CreateNavigator();

//создать XPathNodeIterator узлов book,

// которые имеют novel значением атрибута genre

XPathNodeIterator iter=nav.Select("/bookstore/book[@genre="novel']");

while(iter.MoveNext()) {

LoadBook(iter.Current.Clone());

}

// добавим разделительную линию и вычислим сумму

listBox1.Items.Add("========================");

listBox1.Items.Add("Total Cost = "

+ nav.Evaluate("sum(/bookstore/book[@genre='novel']/price)"));

}

При этом вывод изменится следующим образом:

XslTransform

Пространство имен System.Xml.Xsl содержит классы XSL, применяемые .NET. XslTransform может использоваться с любым хранилищем, которое реализует интерфейс IXPathNavigable. В настоящее время на платформе .NET это: XmlDocument, XmlDataDocument и XPathDocument. Так же как и в случае XPath, воспользуйтесь тем хранилищем, которое подходит лучшим образом. Если планируется создание заказного хранилища, такого как файловая система, и желательно иметь возможность выполнять преобразования, не забудьте реализовать в классе интерфейс IXPathNavigable.

XslTransform основывается на потоковой модели запросов. В связи с этим можно соединить несколько преобразования вместе. Можно даже применять, если нужно, между преобразованиями заказной объект чтения. Это предоставляет большую гибкость при проектировании.

В первом примере, который мы рассмотрим, берется документ books.xml и преобразуется в простой документ HTML для вывода. (Этот код можно найти в папке XPathXSLSample3.) Необходимо будет добавить следующие операторы using:

using System.IO;

using System.Xml.Xsl;

using System.Xml.XPath;

Вот код, выполняющий преобразование:

private void button1_Click(object sender System.EventArgs e) {

//создать новый XPathDocument

XPathDocument doc=new XPathDocument("..\\..\\..\\booksxpath.XML");

// создать новый XslTransForm

XslTransform transForm=new XslTransform();

transForm.Load("..\\..\\..\\books.xsl");

// этот FileStream будет нашим выводом

FileStream fs=new FileStream("..\\..\\..\\booklist.html", FileMode.Create);

// Создать Navigator

XPathNavigator nav=((IXPathNavigable)doc).CreateNavigator();

// Выполнить преобразование. Файл вывода создается здесь.

transForm.Transform(nav, null, fs);

}

Сделать это преобразование проще почти невозможно. Сначала создается объект на основе XPathDocument и объект на основе XslTransform. Затем файл bookspath.xml загружается в doc, a books.xsl в transForm. В этом примере для записи нового документа HTML на диск создается объект FileStream.

Если бы это было приложение ASP.NET, мы использовали бы объект TextWriter и передавали бы его в объект HttpResponse. Если бы мы преобразовывали в другой документ XML, то применялся бы объект на основе XmlWriter. После того как объекты XPathDocument и XslTransform будут готовы, мы создаем XPathNavigator на doc и передаем nav и этот stream в метод Transform объекта transForm. XslTransform имеет несколько перегружаемых версий, получающих комбинации навигаторов, XsltArgumentList (подробнее об этом позже) и потоков ввода/вывода. Параметром навигатора может быть XPathNavigator или любой объект, реализующий интерфейс IXPathNavigable. Потоки ввода/вывода могут быть TextWriter, Stream или объектом на основе XmlWriter.

Документ books.xsl является таблицей стилей. Документ выглядит следующим образом:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">

<html>

<head>

<title>Price List</title>

</head>

<body>

<table>

<xsl:apply-templates/>

</table>

</body>

</html>

</xsl:template>

<xsl:template match="bookstore">

<xsl:apply-templates select= "book"/>

</xsl:template>

<xsl:template match="book">

<tr><td>

<xsl:value-of select="title"/>

</td><td>

<xsl:value-of select="price"/>

</td></tr>

</xsl:template>

</xsl:stylesheet>

Ранее упоминался объект XsltArgumentList. Это способ, которым можно объект с методами связать с пространством имен. Когда это сделано, можно вызывать методы во время преобразования. Рассмотрим пример, чтобы понять, как это работает (находится в XPathXSLSample4):

private void button1_Click(object sender, System.EventArgs e) {

// новый XPathDocument

XPathDocument doc=new XPathDocument("..\\..\\..\\booksxpath.xml");

// новый XslTransform

XslTransform transForm=new XslTransform();

transForm.Load("..\\..\\..\\booksarg.xsl");

// новый XmlTextWriter, так как мы создаем новый документ xml

XmlWriter xw=new XmlTextWriter(..\\..\\..\\argSample.xml", null);

// создать XslArgumentList и новый объект BookUtils

XsltArgumentList argBook=new XsltArgumentList();

BookUtils bu=new BookUtils();

// это сообщает список аргументов BookUtils

argBook.AddExtensionObject("urn:ProCSharp", bu);

// новый XPathNavigator

XPathNavigator nav=((IXPathNavigable)doc).CreateNavigator();

// выполнить преобразование

transForm.Transform(nav, argBook, xw);

xw.Close();

}

// простой тестовый класс

public class BookUtils {

public BookUtils() {}

public string ShowText() {

return "This came from the ShowText method!";

}

}

Вывод преобразования (argSample.xml) выглядит так:

<?xml version="1.0"?>

<books>

<discbook>

<booktitle>The Autobiography of Benjamin Franklin</booktitle>

<showtext>This came from the ShowText method!</showLext>

</discbook>

<discbook>

<booktitle>The Confidence Man</booktitle>

<showtext>This came from the ShowText method!</showtext>

</discbook>

<discbook>

<booktitle>The Gorgias</booktitle>

<showtext>This came from the ShowText method!</showtext>

</discbook>

<discbook>

<booktitle>The Great Cookie Caper</booktitle>

<showtext>This came from the ShowText method!</showtext>

</discbook>

<discbook>

<booktitle>A Really Great Book</booktitle>

<showtext>This came from the ShowText method!</showtext>

</discbook>

</books>

Определим новый класс BookUtils. В этом классе мы имеем один практически бесполезный метод, который возвращает строку "This came from the ShowText method!". Для события button1_Click создаются XPathDocument и XslTransform так же, как это делалось раньше, но с некоторыми исключениями. В этот раз мы собираемся создать документ XML, поэтому используем XMLWriter вместо FileStream. Вот эти изменения:

XsltArgumentList argBook=new XsltArgumentList();

BookUtils bu=new BookUtils();

argBook.AddExtensionObject("urn:ProCSharp", bu);

Именно здесь создается XsltArgumentList. Мы создаем экземпляр объекта BookUtils, и когда вызывается метод AddExtensionObject, ему передается пространство имен расширения и объект, из которого мы хотим вызывать методы. Когда делается вызов Transform, ему передаются XsltArgumentList (argBook) вместе с XPathNavigator и созданный объект XmlWriter. Вот документ booksarg.xsl:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:bookutil="urn:ProCSharp">

<xsl:output method="xml" indent="yes"/>

<xsl:template match="/">

<xsl:element name="books">

<xsl:apply-templates/>

</xsl:element>

</xsl:template>

<xsl:template match="bookstore">

<xsl:apply-templates select="book"/>

</xsl:template>

<xsl:template match="book">

<xsl:element name="discbook">

<xsl:element name="booktitle">

<xsl:value-of select="title"/>

</xsl:element>

<xsl:element name="showtext">

<xsl:value-of select="bookUtil:ShowText()"/>

</xsl:element>

</xsl:element>

</xsl:template>

</xsl:stylesheet>

Здесь имеются две важные строки. В начале добавляется пространство имен, которое создается при добавлении объекта к XsltArgumentList. Затем применяется стандартный синтаксис использования префикса перед пространством имен XSLT и вызывается метод.

Иначе это можно было бы выполнить с помощью сценария XSLT. В таблицу стилей можно включить код C#, VB и JavaScript. Большим достоинством этого является то, что в отличие от текущих реализаций, сценарий компилируется при вызове Transform.Load; таким образом выполняются уже откомпилированные сценарии, в значительной степени так же, как работает ASP.NET. Давайте выполним предыдущий пример таким способом. Добавим сценарий к таблице стилей. Эти изменения можно увидеть в файле bookscript.xsl:

<xsl:stylesheet version="1.0" xmlns:Xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:user="http://wrox.com">

<msxsl:script language="C#" implements-prefix="user">

string ShowText() {

return "This came from the ShowText method!";

}

</msxsl:script>

<xsl:output method="xml" indent="yes"/>

<xsl:template match="/">

<xsl:element name="books">

<xsl:apply-templates/>

</xsl:element>

</xsl:template>

<xsl:template match="bookstore">

<xsl:apply-templates select="book"/>

</xsl:template>

<xsl:template match="book">

<xsl:element name="discbook">

<xsl:element name="booktitle">

<xsl:value-of select="title"/>

</xsl:element>

<xsl:element name="showtext">

<xsl:value-of select="user:ShowText()"/>

</xsl:element>

</xsl:element>

</xsl:template>

</xsl:stylesheet>

Изменения включают задание пространства имен сценариев, добавление кода (который скопирован из VS.NET IDE) и выполнение вызова в таблице стилей. Вывод выглядит так же, как и в предыдущем примере.

Ключевой момент, о котором необходимо помнить при выполнении преобразований, состоит в том, чтобы не забыть использовать подходящее хранилище; XPathDocument, если не требуется редактирование, XmlDataDocument, если данные получают из ADO.NET, и XmlDocument, если необходимо иметь возможность редактировать данные. Процесс будет таким же, несмотря ни на что.

XML и ADO.NET

XML является средством, которое связывает ADO.NET с остальным миром. ADO.NET был создан для работы внутри среды XML. XML используется для преобразования данных в и из хранилища данных в приложение или страницу Web. Так как ADO.NET использует XML в качестве транспорта, то данными можно обмениваться с приложениями и системами, которые даже не знают об ADO.NET. Пока обрабатывается XML, они могут совместно использовать данные. ADO.NET может читать документы XML, возвращаемые из этих же приложений. В связи с важностью XML для ADO.NET, существует ряд полезных свойств ADO.NET, которые позволяют чтение и запись документов XML. Пространство имен XML содержит также классы, которые могут потреблять или утилизировать реляционные данные ADO.NET.

Данные ADO.NET в документе XML

Первый пример, который будет рассмотрен, использует потоки ADO.NET и XML для извлечения данных из базы данных Northwind в DataSet, загрузки объекта XmlDocument, содержащего XML, из DataSet, и загрузки XML в listbox аналогично тому, что делалось ранее. Чтобы выполнить несколько следующих примеров, необходимо добавить инструкции using:

using System.Data;

using System.Xml;

using System.Data.SqlClient;

using System.IO;

Также для примеров ADO в формы добавлены DataGrid, что позволит нам увидеть данные в DataSet из ADO.NET, так как они ограничены сеткой, а также данные из созданных документов XML, которые загружаются в listbox. Вот код первого примера, который можно найти в папке ADOSample1:

private void button1_Click(object sender, System.EventArgs e) {

// создать множество данных DataSet

DataSet ds=new DataSet("XMLProducts");

// соединиться с базой данных northwind и

//выбрать все строки из таблицы продуктов

//убедитесь, что имя пользователя соответствует версии SqlServer

SqlConnection conn=

new SqlConnection(@"server=GLYNNJ_CS\NetSDK;uid=sa;pwd=;database=northwind");

SqlDataAdapter da=new SqDataAdapter("select * from products", conn);

После создания SqlDataAdapter, da и DataSet, ds создаются экземпляры объекта MemoryStream, объекта StreamReader и объекта StreamWriter. Объекты StreamReader и StreamWriter будут применять MemoryStream для перемещения XML:

MemoryStream memStrm=new MemoryStream();

StreamReader strmRead=new StreamReader(memStrm);

StreamWriter strmWrite=new StreamWriter(memStrm);

Мы будем использовать MemoryStream, поэтому ничего на диск записываться не будет, однако мы сможем применять любые объекты на основе класса Stream, такие как FileStream. Затем мы заполним DataSet и свяжем его с DataGrid. Данные из DataSet будут выводиться теперь в DataGrid:

da.Fill(ds, "products");

// загрузка данных в DataGrid

dataGrid1.DataSource=ds;

dataGrid1.DataMember="products";

На следующем шаге генерируется XML. Вызывается метод WriteXml из класса DataSet. WriteXml генерирует документ XML. Существуют две перегружаемые версии WriteXml, одна получает строку с путем доступа и именем файла, а в другом методе добавлен параметр режима mode. Этот mode является перечислением XmlWriteMode. Возможными значениями являются DiffGram, IgnoreSchema, и WriteSchema. Обсудим DiffGram позже в этом разделе. IgnoreSchema используется, если нежелательно, чтобы WriteXml записывал подставляемую (inline) схему в файл XML; используйте параметр WriteSchema, если это желательно. Чтобы получить именно схему, вызывается WriteXmlSchema. Этот метод имеет четыре перегружаемые версии. Одна получает строку, содержащую путь доступа и имя файла, куда записывается документ XML. Вторая версия использует объект, который основывается на классе XmlWriter. Третья версия использует объект, который основывается на классе TextWriter. Четвертая версия используется в примере, параметр в этом случае является производным от класса Stream:

ds.WriteXml(strmWrite, XmlWriteMode.IgnoreSchema);

memStrm.Seek(0, SeekOrigin, Begin);

// читаем из потока в памяти в объект XmlDocument

doc.load(strmRead);

// получить все элементы продуктов

XmlNodeList nodeLst=doc.GetElementsByTagName("ProductName");

// загрузить их в окно списка

foreach(XmlNode nd in nodeLst) listBox1.Items.Add(nd.InnerText);

}

private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e) {

// при щелчке в окне списка

// появляется окно сообщения с ценой изделия

string srch=

"XmlProducts/products[ProductName= " + '"' + listBox1.SelectedItem.ToString() + "]";

XmlNode foundNode=doc.SelectSingleNode(srch);

if (foundNode!=null)

MessageBox.Show(foundNode.SelectSingleNode("UnitPrice").InnerText);

else MessageBox.Show("Not found");

}

На следующем экране можно видеть данные в списке, а также в таблице данных:

Если желательно сохранить документ XML на диске, то нужно сделать примерно следующее:

string file = "с:\\test\\product.xml";

ds.WriteXml(file);

Это даст нам правильно сформированный документ XML на диске, который можно прочитать посредством другого потока, с помощью DataSet, или может использоваться другим приложением или web-сайтом. Так как никакого параметра XmlMode не определено, этот документ XmlDocument будет содержать схему. В нашем примере в качестве параметра для метода XmlDocument.Load используется поток.

Когда XmlDocument подготовлен, мы загружаем listbox с помощью того же объекта XPath, который использовался раньше. Если посмотреть внимательно, то можно заметить, что слегка изменено событие listBox1_SelectedIndexChanged. Вместо вывода InnerText элемента, выполняется другой поиск XPath с помощью SelectSingleNode, чтобы получить элемент UnitPrice. Каждый раз при щелчке на продукте в listbox будет появляться MessageBox для UnitPrise. Теперь у нас есть два представления данных, но более важно то, что имеется возможность манипулировать данными с помощью двух различных моделей. Можно использовать пространство имен Data для данных или пространство имен XML через данные. Такой подход ведет к очень гибким конструкциям в приложениях, так как теперь при программировании нет жесткой связи только с одной объектной моделью. Таким образом, мы имеем несколько представлений одних и тех же данных и несколько способов доступа к данным.

Следующий пример будет упрощать процесс, удаляя три потока и используя некоторые возможности ADO, встроенные в пространство имен XML. Нам понадобится изменить строку кода на уровне модуля:

private XmlDocument doc=new XmlDocument();

на:

private XmlDataDocument doc;

Это нужно сделать, так как мы не собираемся использовать XmlDataDocument. Вот код, который можно найти в папке ADOSample2:

private void button1_Click(object sender, System.EventArgs e) {

// создать множество данных (DataSet)

DataSet ds=new DataSet("XMLProducts");

// соединиться с базой данных northwind и

//выбрать все строки из таблицы products

//выполнить изменения в строке подключения с учетом имени пользователя и имени сервера

SqlConnection conn=

new SqlConnection(@"server=GLYNNJ_CS\NetSDK;uid=sa;pwd=;database=northwind");

SqlDataAdapter da=new SqlDataAdapter("select * from products", conn);

// заполнить множество данных

da.Fill(ds, "products");

// загрузить данные в сетку

dataGrid1.DataSource=ds;

dataGrid1.DataMember="products";

doc=new XmlDataDocument(ds);

// извлечь все элементы продуктов

XmlNodeList nodeLst=doc.GetElementsByTagName("ProductName");

// загрузить их в окно списка

// здесь используется цикл for

for(int ctr=0; ctr<nodeLst.Count; ctr++) listBox1.Items.Add(nodeLst[ctr].InnerText);

}

Как можно видеть, код для загрузки DataSet в документ XML был упрощен. Вместо использования класса XmlDocument, используется класс XmlDataDocument. Этот класс был создан специально для использования данных с объектом DataSet.

XmlDataDocument базируется на классе XmlDocument, поэтому он имеет всю функциональность класса XmlDocument. Одним из основных отличий является перегруженный конструктор для XmlDataDocument. Отметим строку кода, где создается экземпляр XmlDataDocument:

XmlDataDocument doc=new XmlDataDocument(ds);

Он передает в качестве параметра созданный объект DataSet, ds. Документ XML создается из множества данных, поэтому не требуется использование метода Load. Существует также свойство DataSet, которое может задаваться с помощью текущего свойства DataSet. Фактически, если создается новый объект XmlDataDocument без передачи DataSet в качестве параметра, то он содержит объект DataSet с именем NewDataSet, который не имеет DataTables в коллекции таблиц. Существует также свойство DataSet, которое можно установить после создания объекта на основе XmlDataDocument. Если после вызова DataSet.Fill добавляется следующая строка кода:

ds.WriteXml("с:\\test\\sample.xml" , XmlWriteMode, WriteSchema);

…создается следующий XML. Отметим, что мы включили в документ схему XSD. Если нежелательно, чтобы схема включалась в файл, то можно передать член перечисления XmlWriteMode.IgnoreSchema:

<?xml version="1.0" standalone="yes"?>

<XMLProducts>

<xsd:schema id="XMLProducts" targetNamespace="" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">

<xsd:element name="XMLProducts" msdata:IsDataSet="true">

<xsd:complexType>

<xsd:choice maxOccurs="unbounded">

<xsd:element name="products">

<xsd:complexType>

<xsd:sequence>

<xsd:element name="ProductID" type="xsd:int" minOccurs="0" />

<xsd:element name="ProductName" type="xsd:string" minOccurs="0" />

<xsd:element name="SupplierID" type="xsd:int" minOccurs ="0" />

<xsd:element name="CategoryID" type="xsd:int" minOccurs="0" />

<xsd:element name="QuantityPerUnit" type="xsd:string" minOccurs="0" />

<xsd:element name="UnitPrice" type="xsd:decimal" minOccurs="0" />

<xsd:element name="UnitsInStock" type="xsd:short" minOccurs="0" />

<xsd:element name="UnitsOnOrder" type="xsd:short" minOccurs="0" />

<xsd:element name="ReorderLevel" type="xsd:short" minOccurs="0" />

<xsd:element name="Discontinued" type="xsd:boolean" minOccurs="0" />

</xsd:sequence>

</xsd:сomplexType>

</xsd:element>

</xsd:choice>

</xsd:complexType>

</xsd:element>

</xsd:schema>

<products>

<ProductID>1</ProductID>

<ProductName>Chai</ProductName>

<SupplierID>1</SupplierID>

<CategoryID>1</CategoryID>

<QuantityPerUnit>10 boxes x 20 bags</QuantityPerUnit>

<UnitPrice>18</UnitPrice>

<UnitsInStock>39</UnitsInStock>

<UnitsOnOrder>0</UnitsOnOrder>

<ReorderLevel>10</ReorderLevel>

<Discontinued>false</Discontinued>

</products>

<products>

<ProductID>2</ProductID>

<ProductName>Chang</ProductName>

<SupplierID>1</SupplierID>

<CategoryID>1</CategoryID>

<QuantityPerUnit>24 - 12 oz bottles</QuantityPerUnit>

<Unitprice>19</UnitPrice>

<UnitsInStock>17</UnitsInStock>

<UnitsOnOrder>40</UnitsOnOrder>

<ReorderLevel>25</ReorderLevel>

<Discontinued>false</Discontinued>

</products>

</XMLProducts>

Показаны только два первых продукта. Реальный файл XML будет содержать все продукты из таблицы Products базы данных Northwind.

Это выглядит достаточно просто для одной таблицы, но что будет для реляционных данных, таких как несколько DataTables и Relations в DataSet? Все по-прежнему работает таким же образом. Внесем следующие изменения в коде (эту версию можно найти в ADOSample3):

private void button1_Click(object sender, System.EventArgs e) {

//создать множество данных (DataSet)

DataSet ds=new DataSet("XMLProducts");

// соединиться с базой данных northwind и

//выбрать все строки из таблицы products и таблицы suppliers

//проверьте, что строка соединения соответствует конфигурации сервера

SqlConnection conn=

new SqlConnection(@"server=GLYNNJ_CS\NetSDK;uid=sa;pwd=;database=northwind");

SqlDataAdapter daProd=new SqlDataAdapter("select * from products", conn);

SqlDataAdapter daSup=new SqlDataAdapter("select * from suppliers", conn);

//Заполнить DataSet из обоих SqlAdapters

daProd.Fill(ds, "products");

daSup.Fill(ds, "suppliers");

//Добавить отношение

ds.Relations.Add(ds.Tables["suppliers"].Columns["SupplierId"],

ds.Tables["products"].Columns["SupplierId"]);

//Записать Xml в файл, чтобы можно было просмотреть его позже

ds.WriteXml("..\\..\\..\\SuppProd.xml", XmlWriteMode.WriteSchema);

//загрузить данные в таблицу

dataGrid1.DataSource=ds;

dataGrid1.DataMember="suppliers";

//создать XmlDataDocument

doc=new XmlDataDocument(ds);

//Выбрать элементы productname и загрузить их в таблицу

XmlNodeList nodeLst=doc.SelectNodes("//ProductName");

foreach(XmlNode nd in nodeLst) listBox1.Items.Add(nd.InnerXml);

}

В этом примере создаются два объекта DataTables в DataSet из XMLProducts: Products и Suppliers. Отношение состоит в том, что Suppliers (Поставщики) поставляют Products (Продукты). Мы создаем новое отношение на столбце SupplierId в обоих таблицах. Вот как выглядит DataSet:

Делая такой же вызов метода WriteXml, как в предыдущем примере, мы получим следующий файл XML (SuppProd.xml):

<?xml version="1.0" standalone="yes"?>

<XMLProducts>

<xsd:schema id="XMLProducts" targetNamespace="" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">

<xsd:element name="XMLProducts" msdata:IsDataSet="true">

<xsd:complexType>

<xsd:choice maxOccurs="unbounded">

<xsd:element name="products">

<xsd:complexType>

<xsd:sequence>

<xsd:element name="Product ID" type="xsd:int" minOccurs="0" />

<xsd:element name="ProductName" type="xsd:string" minOccurs="0" />

<xsd:element name="SupplierID" type="xsd:int" minOccurs="0" />

<xsd:element name="CategoryID" type="xsd:int" minOccurs="0" />

<xsd:element name="QuantityPerUnit" type="xsd:string" minOccurs="0" />

<xsd:element name="UnitPrice" type="xsd:decimal" minOccurs="0" />

<xsd:element name="UnitsInStock" type="xsd:short" minOccurs="0" />

<xsd:element name="UnitsOnOrder" type="xsd:short" minOccurs="0" />

<xsd:element name="ReorderLevel" type="xsd:short" minOccurs="0" />

<xsd:element name="Discontinued" type="xsd:boolean" minOccurs="0" />

</xsd:sequence>

</xsd:complexType>

</xsd:element>

<xsd:element name="suppliers">

<xsd:complexType>

<xsd:sequence>

<xsd:element name="SupplierID" type="xsd:int" minOccurs="0" />

<xsd:element name="CompanyName" type="xsd:string" minOccurs="0" />

<xsd:element name="ContactName" type="xsd:string" minOccurs="0" />

<xsd:element name="ContactTitle" type="xsd:string" minOccurs="0" />

<xsd:element name="Address" type="xsd:string" minOccurs="0" />

<xsd:element name="City" type="xsd:string" minOccurs="0" />

<xsd:element name="Region" type="xsd:string" minOccurs="0" />

<xsd:element name="PostalCode" type="xsd:string" minOccurs="0" />

<xsd:element name="Country" type="xsd:string" minOccurs="0" />

<xsd:element name="Phone" type="xsd:string" minOccurs="0" />

<xsd:element name="Fax" type="xsd:string" minOccurs="0" />

<xsd:element name="HomePage" type="xsd:string" minOccurs="0" />

</xsd:sequence>

</xsd:complexType>

</xsd:element>

</xsd:choice>

</xsd:complexType>

<xsd:unique name="Constraint1">

<xsd:selector xpath=".//suppliers" />

<xsd:field xpath="SupplierID" />

</xsd:unique>

<xsd:keyref name="Relation1" refer="Constraint1">

<xsd:selector xpath=".//products" />

<xsd:field xpath="SupplierID" />

</xsd:keyref>

</xsd:elements>

</xsd:schema>

<products>

<ProductID>1</ProductID>

<ProductName>Chai</ProductName>

<SupplierID>1</SupplierID>

<CategoryID>1</CategoryID>

<QuantityPerUnit>10 boxes x 20 bags</QuantityPerUnit>

<UnitPrice>18</UnitPrice>

<UnitsInStock>39</UnitsInStock>

<UnitsOnOrder>0</UnitsOnOrder>

<ReorderLevel>10</ReorderLevel>

<Discontinued>false</Discontinued>

</products>

<products>

<ProductID>2</ProductID>

<ProductName>Chang</ProductName>

<SupplierID>1</SupplierID>

<CategoryID>1</CategoryID>

<QuantityPerUnit>24 - 12 oz bottles</QuantityPerUnit>

<UnitPrice>19</UnitPrice>

<UnitsInStock>17</UnitsInStock>

<UnitsOnOrder>40<UnitsOnOrder>

<ReorderLevel>25</ReorderLevel>

<Discontinued>false</Discontinued>

</products>

<suppliers>

<SupplierID>1</SupplierID>

<CompanyName>Exotiс Liquids</CompanyName>

<ContactName>Charlotte Cooper</ContactName>

<ContactTitle>Purchasing Manager</ContactTitle>

<Address>49 Gilbert St.</Address>

<City>London</City>

<PostalCode>EC1 4SD</PostalCode>

<Country>UK</Country>

<Phone>(171) 555-2222</Phone>

</suppliers>

<suppliers>

<Supplier ID>2</SupplierID>

<CompanyName>New Orleans Cajun Delights</CompanyName>

<ContactName>Shelley Burke</ContactName>

<ContactTitle>Order Adminisirator</ContactTitle>

<Address>P.O. Box 78934</Address>

<City>New Orleans</City>

<Region>LA</Region>

<PostalCode>70117</PostalCode>

<Country>USA</Country>

<Phone>(100) 555-4822</Phone>

<HomePage>#CAJUN.HTM#</HomePage>

</suppliers>

</XMLProducts>

Эта схема включает в себя обе таблицы данных DataTables, которые находились в DataSet. Данные также содержат все данные из обеих таблиц. Несколько продуктов и поставщиков были удалены из окончательного файла, чтобы сэкономить пространство. Как и раньше, можно сохранить только схему или только данные, передавая соответствующий параметр XmlWriteMode.

Преобразование документа XML в данные ADO.NET

Предположим что имеется документ XML, который нужно поместить в DataSet ADO.NET. И вы хотите сделать это так. чтобы можно было загрузить XML в базу данных, или, может быть, связать данные с управляющим элементом данных .NET, таким как DataGrid. Таким образом, можно будет на самом деле использовать документ XML в качестве хранилища данных, и можно будет полностью исключить накладные расходы, связанные с базой данных. Вот некоторый код для начала (ADOSample4):

private void button1_Click(object sender, System.EventArgs e) {

// создать новое множество данных (DataSet)

DataSet ds=new DataSet("XMLProducts");

//считать документ Xml в Dataset

ds.ReadXml("..\\..\\..\\prod.xml");

//загрузить данные в таблицу

detaGrid1.DataSource=ds;

dataGrid1.DataMember="products";

//создать новый XmlDataDocument

doc=new XmlDataDocument(ds);

//загрузить имена продуктов в окно списка

XmlNodeList nodeLst=doc.SelectNodes("//ProductName");

foreach(XmlNode nd in nodeLst) listBox1.Items.Add(nd.InnerXml);

}

Действительно, просто. Создается экземпляр нового объекта DataSet. Вызывается метод ReadXml, и XML оказывается в DataTable в DataSet. Как и методы WriteXml, ReadXml имеет параметр XmlReadMode и пару дополнительных опций в XmlReadMode. Они приводятся в следующей таблице:

Имя перечисления

Описание

Auto

Задает для XmlReadMode наиболее подходящее значение. Если данные находятся в формате DiffGram, выбирается DiffGram. Если схема уже была прочитана, или если обнаружена подставляемая схема, то выбирается ReadSchema. Если с DataSet не связано ни одной схемы и не обнаружена подставляемая схема, то выбирается IgnoreSchema.

DiffGram

Считывает в документ DiffGram и применяет изменения к DataSet. DiffGram описан далее в этой главе.

Fragment

Считывает документы, которые содержат фрагменты схемы XDR, такие как тип, созданный SQL Server.

IgnoreSchema

Игнорирует подставляемую схему, которая может быть обнаружена. Считывает данные в текущую схему DataSet. Если данные не соответствуют схеме DataSet, они отбрасываются.

InferSchema

Игнорирует любую подставляемую схему. Создает схему на основе данных в документе XML. Если она существует в DataSet, используется эта схема, расширяемая дополнительными столбцами и таблицами. Если столбец существует, но имеет другой тип данных, порождается исключение.

ReadSchema

Считывает подставляемую схему и загружает данные. Не будет перезаписывать схему в DataSet, но будет порождать исключение, если таблица в подставляемой схеме уже существует в DataSet.

Существует также метод ReadSchema. Он будет считывать автономную схему и создавать таблицы, столбцы и отношения соответственно. Этот метод используется, если схема не поставляется вместе с данными. ReadSchema имеет те же четыре перегружаемые версии, строку с именем файла и путем доступа, объект на основе Stream, объект на основе TextReader и объект на основе XmlReader.

Чтобы показать, что таблицы данных будут созданы правильно, загрузим документ XML, который содержит таблицы Products и Suppliers, использовавшиеся в предыдущем примере. В этот раз, однако, загрузим в listbox имена DataTable, имена DataColumn и тип данных. Мы можем сравнить это с первоначальной базой данных Northwind, чтобы убедиться, что все по-прежнему хорошо. Вот код, который будет применяться и который можно найти в ADOSample5:

private void button1_Click(object sender, System.EventArgs e) {

// создать DataSet

DataSet ds=new DataSet("XMLProducts");

// считать документ xml

ds.ReadXml("..\\..\\..\\SuppProd.xml");

// загрузить данные в сетку

dataGrid1.DataSource=ds;

dataGrid1.DataMember="products";

// загрузить в listbox информацию о таблицах, столбцах и типах данных

foreach(DataTable dt in ds.Tables) {

listBox1.Items.Add(dt.TableName);

foreach(DataColumn col in dt.Columns) {

listBox1.Items.Add('\t' + col.ColumnName + " - " + col.DataType.FullName);

}

}

}

Отметим добавление двух циклов foreach (выделенных полужирным шрифтом). Первый цикл извлекает имя таблицы из каждой таблицы в коллекции таблиц DataSet. Внутри цикла foreach извлекаются имя и тип данных каждого столбца в DataTable. Эти данные загружаются в listbox, чтобы их можно было вывести. Вот что мы должны увидеть на экране:

Посмотрев на listbox, можно увидеть, что DataTables были созданы с помощью столбцов, которые имеют правильные имена и типы данных.

Кроме того, можно заметить, что последние два примера не преобразуют никаких данных в или из базы данных, таким образом, не определены SqlDataAdapters или SqlConnections. Это дает начальное представление о реальной гибкости пространства имен System.Xml и ADO.NET. Можно посмотреть на одни и те же данные в множестве форматов. Если понадобиться сделать преобразование и показать данные в формате HTML или необходимо связать их с сеткой, возьмите те же самые данные и с помощью вызова метода получите их в нужном формате.

Запись и чтение DiffGram

DiffGram является документом XML, который содержит данные до и после сеанса редактирования, включающего любую комбинацию изменений данных, добавлений и удалений. DiffGram может использоваться как аудиторский журнал или для процесса подтверждения/отката. Большинство систем DBMS сегодня имеют такое встроенное средство. Но если придется работать с DBMS, которая не имеет таких свойств или если хранилищем данных является XML и отсутствует DBMS, то можно будет самостоятельно реализовать свойства подтверждения/отката.

Далее представлен код, показывающий, как DiffGram создается и как DataSet можно создать из DiffGram:pwd (он находится в папке ADOSample6). Начальная часть этого кода должна быть уже знакома. Мы определяем и задаем новый объект DataSet, ds, новый объект SqlConnection, conn и новый объект SqlDataAdapter, da. Мы соединяемся с базой данных, выбираем все строки из таблицы Products, создаем новый объект DataTable с именем products и загружаем данные из базы данных в DataSet:

private void button1_Click(object sender, System.EventArgs e) {

// новый объект DataSet

DataSet ds=new DataSet("XMLProducts");

// Сделать соединение и загрузить строки продуктов

SqlConnection conn=

new SqlConnection(@"server=GLYNNJ_CS\NetSDK;uid=sa;pwd=;database=northwind");

SqlDataAdapter da=new SqlDataAdapter("select * from products", conn);

// заполнить DataSet

da.Fill(ds, "products");

// редактируем первую строку

ds.Tables["products"].Rows[0]["ProductName"] = "NewProdName";

В следующем разделе мы сделаем следующие преобразования. Во-первых, изменим столбец ProductName в первой строке на NewProdName. Во-вторых, создадим новую строку в DataTable, задавая значения столбцов и добавляя в конце новую строку данных в DataTable.

// добавить новую строку

DataRow dr=ds.Tables["products"].NewRow();

dr["ProductId"]=100;

dr["CategoryId"]=2;

dr["Discontinued"]=false;

dr["ProductName"]="This is the new product";

dr["QuantityPerUnit"]=12;

dr["ReorderLevel"]=1;

dr["SupplierId"]=12;

dr["UnitPrice"]=23;

dr["UnitsInStock"]=5;

dr["UnitsOnOrder"]=0;

Tables["products"].Rows.Add(dr);

Это интересная часть кода. Прежде всего записывается схема с помощью WriteXmlSchema. Это важно, так как нельзя будет заново считать в DiffGram без схемы. WriteXml с переданным в него параметром XmlWriteMode.DiffGram создает в действительности DiffGram. Следующая строка принимает сделанные изменения. Важно то, что DiffGram создается до вызова AcceptChanges, иначе не будет никакого различия между состоянием до того и после.

// записать схему

ds.WriteXmlSchema("..\\..\\..\\diffgram.xsd");

// создать DiffGram

ds.WriteXml("..\\..\\..\\diffgram.xml", XmlWriteMode.DiffGram);

ds.AcceptChanges();

// загрузить данные в сетку

dataGrid1.DataSource=ds;

dataGrid1.DataMember="products";

// новый объект XmlDataDocument

doc=new XmlDataDocument(ds);

// загрузить имена продуктов в список

XmlNodeList nodeLst=doc.SelectNodes("//ProductName");

foreach (XmlNode nd in nodeLst) listBox1.Items.Add(nd.InnerXml);

Чтобы вернуть данные в множество DataSet, можно сделать следующее:

DataSet dsNew = new DataSet();

dsNew.ReadXmlSchema("..\\..\\..\\diffgram.xsd");

dsNew.XmlRead("..\\..\\..\\diffgram.xml", XmlReadMode.DiffGram);

В этом примере создается новый объект множества данных DataSet, dsNew. Вызов метода ReadXmlSchema создает новый объект DataTable на основе информации схемы. В данном случае он будет клоном DataTable продуктов. Теперь можно считать в DiffGram. DiffGram не содержит информации о схеме, поэтому важно, чтобы объект DataTable был создан и готов к использованию до вызова метода ReadXml. Вот образец того, как выглядит DiffGram (diffgram.xml):

<?xml version="1.0" standalone="yes"?>

<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">

<XMLProducts>

<products diffgr:id="products1" msdata:rowOrder="0" diffgr:hasChanged="modified">

<ProductID>1</ProduсtID>

<ProductName>NewProdName</ProductName>

<SupplierID>1</SupplierID>

<CategoryID>1</CategoryID>

<QuantityPerUnit>10 boxes x 20 bags</QuantityPerUnit>

<UnitPrice>18</UnitPrice>

<UnitsInStock>39</UnitsInStock>

<UnitsOnOrder>0</UnitsOnOrder>

<ReorderLevel>10</ReorderLevel>

<Discontinued>false</Discontinued>

</products>

<products diffgr:id="products2" msdata:rowOrder="1">

<ProductID>2</ProductID>

<ProduсtName>Chang</ProductName>

<SupplierID>1</SupplierID>

<CategoryID>1</CategoryID>

<QuantityPerUnit>24 - 12 oz bottles</QuantityPerUnit>

<UnitPrice>19</UnitPrice>

<UnitsInStock>17</UnitsInStock>

<UnitsOnOrder>40</UnitsOnOrder>

<ReorderLevel>25</ReorderLevel>

<Discontinued>false</Discontinued>

</products>

…

<products diffgr:id="products78" msdata:rowOrder="77" diffgr:hasChanges="inserted">

<ProductID>100</ProductID>

<ProductName>This is a new product</ProductName>

<SupplierID>12</SupplierID>

<CategoryID>2</CategoryID>

<QuantityPerUnit>12</QuantityPerUnit>

<UnitPrice>23</UnitPrice>

<UnitsInStock>5</UnitsInStock>

<UnitsOnOrder>0</UnitsOnOrder>

<ReorderLevel>1</ReorderLevel>

<Discontinued>false</Discontinued>

</products>

</XMLProducts>

<diffgr:before>

<products diffgr:id="products1" msdata:rowOrder="0">

<ProductID>1</ProductID>

<ProductName>Chai </ProductName>

<SupplierID>1</SupplierID>

<CategoryID>1</CategoryID>

<QuantityPerUnit>10 boxes x 20 bugs </QuantityPerUnit>

<UnitPrice>18</UnitPrice>

<UnitsInStock>39</UnitsInStock>

<UnitsOnOrder>0</UnitsOnOrder>

<ReorderLevel>10</ReorderLevel>

<Discontinued>false</Discontinued>

</products>

</diffgr:before>

</diffgr:diffgram>

Заметим, каким образом повторяется каждая строка DataTable, и что существует атрибут diffgr:id для каждого элемента <products>. diffgr является префиксом пространства имен для urn:schemas-microsoft-com:xml-diffgram-v1. Для модифицированной строки и для вставленной строки ADO.NET добавляет атрибут diffgr:hasChanges. Здесь есть также элемент <diffgr:before> после элемента <XMLProducts>, который содержит элемент <products>, указывающий на предыдущее содержание всех модифицированных строк. Для добавленной строки не существует "before", поэтому здесь отсутствует элемент <diffgr:before>, однако он присутствует для модифицированной строки.

После того как DiffGram считан в DataTable, он оказывается в состоянии, в котором он был бы после выполнения изменений в данных перед вызовом AcceptChanges. В этом месте можно на самом деле откатить изменения, вызывая метод RejectChanges. Проверяя свойство DataRow.Item и передавая либо DataRowVersion.Original, либо DataRowVersion.Current, можно увидеть значения в DataTable перед и после изменений.

Сериализация объектов в XML

Сериализация является процессом сохранения объекта на диске. Другая часть приложения или даже другое приложение могут десериализовать объект и он будет в том же состоянии, в каком он был до сериализации. Платформа .NET содержит два способа выполнения сериализации. Рассмотрим пространство имен System.Xml.Serialization.

Как показывает имя, сериализация производится в формате XML. Это делается преобразованием открытых свойств объекта и открытых полей в элементы и/или атрибуты. Сериализатор XML не может преобразовать скрытые данные, а только открытые. Представляйте это как способ сохранения состояния объекта. Если необходимо сохранить скрытые данные, то используйте BinaryFormatter в пространстве имен System.Runtime.Serialization.Formatters.Binary. Можно также:

□ Определить, должны ли данные быть атрибутом или элементом.

□ Определить пространство имен.

□ Изменить имя атрибута или элемента.

Вместе с возможностью сериализовать только открытые данные, невозможно сериализовать графы объектов (объекты, которые достижимы из сериализуемого объекта). Это не является серьезным ограничением. При тщательном проектировании классов этого легко можно избежать. Если необходимо иметь возможность сериализовать открытые и скрытые данные, а также граф объектов, содержащий множество вложенных объектов, то можно будет воспользоваться пространством имен System.Runtime.Serialization.Formatters.Binary.

Данные для сериализации могут быть примитивными типами данных, полями, массивами и XML, встроенным в форму объектов XmlElement и XmlAttribute. Связью между объектом и документом XML являются специальные атрибуты, которые аннотируют классы. Эти атрибуты используются для того, чтобы информировать сериализатор, как записать данные.

На платформе .NET существует инструмент, помогающий создавать атрибуты,— это утилита xsd.exe, которая может делать следующее:

□ Генерировать схему XML из файла схемы XDR

□ Генерировать схему XML из файла XML

□ Генерировать классы DataSet из файла схемы XSD

□ Генерировать классы времени выполнения, которые имеют специальные атрибуты для XmlSerilization

□ Генерировать XSD из классов, которые уже были разработаны

□ Ограничивать список элементов, которые создаются в коде

□ Определять, на каком языке программирования должен быть представлен генерируемый код (C#, VB.NET, или JScript.NET)

□ Создавать схемы из типов в компилированных сборках

В документации платформы можно найти подробное описание параметров командной строки.

Несмотря на предлагаемые возможности, вовсе не обязательно использовать xsd.exe, чтобы создать классы для сериализации. Рассмотрим простое приложение, которое сериализует класс, считывающий данные о продуктах, сохраненных ранее в этой главе (пример находится в папке SerialSample1). В начале идет очень простой код, который создает новый объект Product, pd, и записывает некоторые данные:

private void button1_Click(object sender, System.EventArgs e) {

// новый объект Products

Products pd=new Products();

// задать некоторые свойства

pd.ProductXD=200;

pd.CategoryID=100;

pd.Discontinued=false;

pd.ProductName="Serialize Objects";

pd.QuantityPerUnit="6";

pd.ReorderLevel=1;

pd.SupplierID=1;

pd.UnitPrice=1000;

pd.UnitsInStock=10;

pd.UnitsOnOrder=0;

Метод Serialize класса XmlSerializer имеет шесть перегружаемых версий. Одним из требуемых параметров является поток для записи в него данных. Это может быть Stream, TextWriter или XmlWriter. В данном случае мы создали объект tr на основе TextWriter. Затем необходимо создать объект sr на основе XmlSerializer. XmlSerializer должен знать информацию о типе сериализуемого объекта, поэтому используется ключевое слово typeof с указанием типа, который должен быть сериализован. После создания объекта sr вызывается метод Serialize, в который передается tr (объект на основе Stream) и объект, который необходимо сериализовать, в данном случае pd. Не забудьте закрыть поток, когда закончите с ним работу.

//новый TextWriter и XmlSerializer

TextWriter tr=new StreamWriter("..\\..\\..\\serialprod.xml");

XmlSerializer sr=new XmlSerializer(typeof(Products));

// сериализуем объект

sr.Serialize(tr,pd);

tr.Close();

}

Здесь мы добавляем событие другой кнопки для создания нового объекта newPd на основе Products. В этот раз мы будем использовать объект FileStream для чтения XML:

private void button2_Click(object sender, System.EventArgs e) {

// создаем ссылку на тип Products Products newPd;

// новый файловый поток для открытия сериализованного объекта

FileStream f=new FileStream("..\\..\\..\\serialprod.xml", FileMode.Open);

Здесь создается новый объект XmlSerializer, таким образом передается информация о типе Product. Затем можно вызвать метод Deserialize. Отметим, что нам по-прежнему необходимо делать явное преобразование типа, когда создается объект newPd. В этом месте newPd имеет такое же состояние, как и pd:

// новый Serializer

XmlSerializer newSr=new XmlSerializer(typeof(Products));

// десериализация объекта

newPd=(Products)newSr.Deserialize(f);

// загружаем его в окно списка.

listBox1.Items.Add(newPd.ProductName);

f.Closed();

}

Теперь мы проверим класс Products. Единственное различие между ним и любым другим классом, который можно записать, состоит в добавленных атрибутах. Не путайте эти атрибуты с атрибутами в документе XML. Эти атрибуты расширяют класс SystemAttribute. Атрибут является некоторой декларативной информацией, которая может извлекаться во время выполнения с помощью CLR (см. в главе 6 более подробно). В данном случае добавляются атрибуты, которые описывают, как объект должен быть сериализован:

//класс, который будет сериализован,

//атрибуты определяют, как объект сериализуется.

[System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)]

public class Products {

[System.Xml.Serialization.XmlElementAttribute(IsNullable=false)]

public int ProductID;

[System.Xml.Serialization.XmlElementAttribute(IsNullable=false)]

public string ProductName;

[System.Xml.Serialization.XmlElementAttribute()]

public int SupplierID;

[System.Xml.Serialization.XmlElementAttribute()]

public int CategoryID;

[System.Xml.Serialization.XmlElementAttribute()]

public string QuantityPerUnit;

[System.Xml.Serialization.XmlElementAttribute()]

public System.Decimal UnitPrice;

[System.Xml.Serialization.XmlElementAttribute()]

public short UnitsInStock;

[System.Xml.Serialization.XmlElementAttribute()]

public short UnitsOnOrder;

[System.Xml.Serialization.XmlElementAttribute()]

public short ReorderLevel;

[System.Xml.Serialization.XmlElementAttribute()]

public bool Discontinued;

}

Созданный документ XML выглядит, как любой другой документ XML, который мы могли бы создать.

<?xml version="1.0" ?>

<Products xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">

<ProductID>200</ProductID>

<ProductName>Serialize Objects</ProductName>

<SupplierID>1</SupplierID>

<CategoryID>100</CategoryID>

<QuantityPerUnit>6</QuantityPerUnit>

<UnitPrice>1000</UnitPrice>

<UnitsInStock>10</UnitsInStock>

<UnitsOnOrder>0</UnitsOnOrder>

<ReorderLevel>1</ReorderLevel>

<Discontinued>false</Discontinued>

</Products>

Здесь нет ничего необычного. Мы могли бы выполнить преобразование документа XML и вывести его как HTML, загрузить его в DataSet с помощью ADO.NET, загрузить с его помощью XmlDocument, как в примере, десериализовать его и создать объект в том же состоянии, которое имел pd перед своей сериализацией (что соответствует событию второй кнопки).

Рассмотренный только что пример является очень простым. Обычно имеется ряд методов получения (get) и задания (set) свойств для работы с данными в объекте. Но что, если объект состоит из двух других объектов, или выводится из базового класса, из которого следуют и другие классы?

Такие ситуации обрабатываются с помощью класса XmlSerializer. Давайте усложним пример (находится в SerialSample2). Для большей реалистичности сделаем каждое свойство доступным через методы get и set:

private void button1_Click(object sender, System.EventArgs e) {

// новый объект products

Products pd=new Products();

// задать некоторые свойства

pd.ProductID=200;

pd.CategoryID=100;

pd.Discontinued=false;

pd.ProductName="Serialize Objects";

pd.QuantityPerUnit="6";

pd.ReorderLevel=1;

pd.SupplierID=1;

pd.UnitPrice=1000;

pd.UnitsInStock=10;

pd.UnitsOnOrder= 0;

pd.Discount=2;

//новые TextWriter и XmlSerializer

TextWriter tr=new StreamWriter("..\\..\\..\\serialprod1.xml");

XmlSerializer sr=new XmlSerializer(typeof(Products));

// сериализируем объект

sr.Serialize(tr, pd);

tr.Close();

}

private void button2_Click(object sender, System.EventArgs e) {

//создать ссылку на тип Products

Products newPd;

// новый файловый поток для открытия сериализуемого объекта

FileStream f=new FileStream("..\\..\\..\\serialprod1.xml", FileMode.Open);

// новый сериализатор

XmlSerializer newSr=new XmlSerializer(typeof(Products));

//десериализуем объект

newPd=(Products)newSr.Deserialize(f);

//загрузить его в окно списка.

listBox1.Items.Add(newPd.ProductName);

f.Close();

}

//класс, который будет сериализован.

//атрибуты определяют, как объект сериализуется

[System.Xml.Serialization.XmlRootAttribute()]

public class Products {

private int prodId;

private string prodName;

private int suppId;

private int catId;

private string qtyPerUnit;

private Decimal unitPrice;

private short unitsInStock;

private short unitsOnOrder;

private short reorderLvl;

private bool discont;

private int disc;

// добавлен атрибут Discount

[XmlAttributeAttribute(AttributeName="Discount")]

public int Discount {

get {return disc;}

set {disc=value;}

}

[XmlElementAttribute()]

public int ProductID {

get {return prodId;}

set {prodId=value;}

}

[XmlElementAttribute()]

public string ProductName {

get {return prodName;}

set {prodName=value;}

}

[XmlElementAttribute()]

public int SupplierID {

get {return suppId;}

set {suppId=value;}

}

[XmlElementAttribute()]

public int CategoryID {

get {return catId;}

set {catId=value;}

}

[XmlElementAttribute()]

public string QuantityPerUnit {

get {return qtyPerUnit;}

set {qtyPerUnit=value;}

}

[XmlElementAttribute()]

public Decimal UnitPrice {

get {return UnitPrice;}

set {unitPrice=value;}

}

[XmlElementAttribute()]

public short UnitsInStock {

get {return unitsInStock;}

set {unitsInStock=value;}

}

[XmlElementAttribute()]

public short UnitsOnOrder {

get {return unitsOrOrder;}

set {unitsOnOrder=value;}

}

[XmlElementAttribute()]

public short ReorderLevel {

get {return reorderLvl;}

set {reorderLvl=value;}

}

[XmlElementAttribute()]

public pool Discontinued {

get {return discont;}

set {discont=value;}

}

}

Выполнение этого кода вместо класса Products в предыдущем примере даст те же самые результаты с одним исключением. Мы добавили атрибут Discount, тем самым показав, что атрибуты также могут быть сериализуемыми. Вывод выглядит следующим образом (serialprod1.xml):

<?xml version="1.0" encoding="utf-8"?>

<Products xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Discount="2">

<ProductID>200</ProductID>

<ProductName>Serialize Objects</ProductName>

<SupplierID>1</SupplierID>

<CategoryID>100</CategoryID>

<QuantityPerUnit>6</QuantityPerUnit>

<UnitPrice>1000</UnitPrice>

<UnitsInStock>10</UnitsInStock>

<UnitsOnOrder>0</UnitsOnOrder>

<ReorderLevel>1</ReorderLevel>

<Discontinued>false</Discontinued>

</Products>

Отметим атрибут Discount элемента Products. Поэтому теперь, когда определены средства задания и получения свойств, можно добавить более сложную проверку кода в свойствах.

Что можно сказать о ситуации, когда имеются производные классы и, возможно, свойства, которые возвращают массив? XmlSerializer также это охватывает. Давайте обсудим более сложную ситуацию.

В событии button1_Click создается новый объект на основе Product и новый объект на основе BookProduct (newProd и newBook). Мы добавляем данные в различные свойства каждого объекта и помещаем объекты в массив на основе Product. Затем создается новый объект на основе Inventory, которому в качестве параметра передается массив. Затем можно сериализовать объект Inventory, чтобы впоследствии его восстановить:

private void button1_Click(object sender, System.EventArgs e) {

// создать новые объекты книги и книжной продукции

Product newProd=new Product();

BookProduct newBook=new BookProduct();

// задать некоторые свойства

newProd.ProductID=100;

newProd.ProductName="Product Thing";

newProd.SupplierID=10;

newBook.ProductID=101;

newBook.ProductName="How to Use Your New Product Thing";

newBook.SupplierID=10;

newBook.ISBN="123456789";

//поместить элементы в массив

Product[] addProd={newProd, newBook};

// новый объект Inventory с помощью массива addProd

Inventory inv=new Inventory();

inv.InventoryItems=addProd;

// сериализуем объект Inventory

TextWriter tr=new StreamWriter("..\\..\\..\\order.xml");

XmlSerializer sr=new XmlSerializer(typeof(Inventory));

sr.Serialize(tr, inv);

tr.Close();

}

Отметим в событии button2_Click, что мы просматриваем массив во вновь созданном объекте newInv, чтобы показать, что это те же данные:

private void button2_Click(object sender, System.EventArgs e) {

Inventory newInv;

FileStream f=new FileStream("..\\..\\..\\order.xml", FileMode.Open);

XmlSerializer newSr=new XmlSerializer(typeof{Inventory));

newInv=(Inventory)newSr.Deserialize(f);

foreach(Product prod in newInv.Inventory Items) listBox1.Items.Add(prod.ProductName);

f.Close();

}

public class inventory {

private Product[] stuff;

public Inventory() {}

Мы имеем XmlArrayItem для каждого типа, который может быть добавлен в массив. Первый параметр определяет имя элемента в создаваемом документе XML. Если опустить этот параметр ElementName, то элементы получат имя типа объекта (в данном случае Product и BookProduct). Существует также класс XmlArrayAttribute, который будет использоваться, если свойство возвращает массив объектов или примитивных типов. Так как мы возвращаем в массиве различные типы, то используется объект XmlArrayItemAttribute, который предоставляет более высокий уровень управления:

// необходимо иметь запись атрибута для каждого типа данных

[XmlArrayItem("Prod", typeof(Product)), XmlArrayItem("Book", typeof(BookProduct))]

//public Inventory(Product [] InventoryItems) {

// stuff=InventoryItems;

//}

public Product[] InventoryItems {

get {return stuff;}

set {stuff=value;}

}

}

//класс Product

public class Product {

private int prodId;

private string prodName;

private int suppId;

public Product() {}

public int ProductID {

get {return prodId;}

set {prodId=value;}

}

public string ProductName {

get {return prodName;}

set {prodName=value;}

}

public int SupplierID {

get {return suppId;}

set {suppId=value;}

}

}

// Класс Bookproduct

public class BookProduct: Product {

private string isbnNum;

public BookProduct() {}

public string ISBN {

get {return isbnNum;}

set {isbnNum=value;}

}

}

В этот пример добавлено два новых класса. Класс Inventory будет отслеживать то, что добавляется на склад. Можно добавлять продукцию на основе класса Product или класса BookProduct, который расширяет Product. В классе Inventory содержится массив добавленных объектов и в нем могут находиться как BookProducts, так и Products. Вот как выглядит документ XML:

<?xml version="1.0" ?>

<Inventory xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">

<InventoryItems>

<Prod>

<ProductID>100</ProductID>

<ProductName>Product Thing</ProductName>

<SupplierID>10</SupplierID>

</Prod>

<Book>

<ProductID>101</ProductID>

<ProductName>How to Use Your New Product Thing</ProductName>

<SupplierID>10</SupplierID>

<ISBN>123456789</ISBN>

</Book>

</InventoryItems>

</Inventory>

Все это работает прекрасно, но как быть в ситуации, когда нет доступа к исходному коду типов, которые будут сериализироваться? Невозможно добавить атрибут, если отсутствует исходный код. Существует другой способ. Можно воспользоваться классами XmlAttributes и XmlAtrtributeOverrides. Вместе эти классы позволят выполнить в точности то, что только что было сделано, но без добавления атрибутов. Вот пример, находящийся в папке SerialSample4:

private void button1_Click(object sender, System.EventArgs e) {

// создать объект XmlAttributes XmlAttributes attrs=new XmlAttributes();

// добавить типы объектов, которые будут сериализированы

attrs.XmlElements.Add(new XmlElementAttribute("Book", typeof(BookProduct)));

attrs.XmlElements.Add(new XmlElementAttribute("Product", typeof(Product)));

XmlAttributeOverrides attrOver=new XmlAttributeOverrides();

//добавить к коллекций атрибутов

attrOver.Add(typeof(Inventory), "InventoryItems", attrs);

// создать объекты Product и Book

Product newProd=new Product();

BookProduct newBook=new BookProduct();

newProd.ProductID=100;

newProd.ProductName="Product Thing";

newProd.SupplierID=10;

newBook.ProductID=101;

newBook.ProductName="How to Use Your New Product Thing";

newBook.SupplierID=10;

newBook.ISBN="123456789";

Product[] addProd={newProd, newBook};

//Product[] addProd={newBook};

Inventory inv=new Inventory();

inv.InventoryItems=addProd;

TextWriter tr=new StreamWriter("..\\..\\..\\inventory.xml");

XmlSerializer sr=new XmlSerializer(typeof(Inventory), attrOver);

sr.Serialize(tr, inv);

tr.Close();

}

private void button2_Click(object sender, System.EventArgs e) {

//необходимо выполнить тот же процесс для десериализации

// создаем новую коллекцию XmlAttributes

XmlAttributes attrs=new XmlAttributes();

// добавляем информацию о типе к коллекции элементов

attrs.XmlElements.Add(new XmlElementAttribute("Book", typeof(BookProduct)));

attrs.XmlElements.Add(new XmlElementAttribute("Product", typeof(Product)));

XmlAttributeOverrides attrOver=new XmlAttributeOverrides();

//добавляем к коллекции Attributes (атрибутов)

attrOver.Add(typeof(Inventory), "InventoryItems", attrs);

//нужен новый объект Inventory для десериализаций в него

Inventory newInv;

// десериализуем и загружаем данные в окно списка из

// десериализованного объекта

FileStream f=new FileStream("..\\..\\..\\inventory.xml", FileMode.Open);

XmlSerializer newSr=new XmlSerializer(typeof(Inventory).attrOver);

newInv=(Inventory)newSr.Deserialize(f);

if (newInv!=null) {

foreach(Product prod in newInv.InventoryItems) listBox1.Items.Add(prod.ProductName);

}

f.Close();

}

// это те же классы, что и в предыдущем примере

// за исключением удаленных атрибутов

// из свойства InventoryItems для Inventory

public class Inventory {

private Product[] stuff;

public Inventory() {}

public Product[] InventoryItems {

get {return stuff;}

set {stuff=value;}

}

}

public class Product {

private int prodId;

private string prodName;

private int suppId;

public Product() {}

public int ProductID {

get {return prodId;}

set {prodId=value;}

}

public string ProductName {

get {return prodName;}

set {prodName=value;}

}

public int SupplierID {

get {return suppId;}

set {suppId=value;}

}

}

public class BookProduct:Product {

private string isbnNum;

public BookProduct() {}

public string ISBN {

get {return isbnNum;}

set {isbnNum=value;}

}

}

Это тот же пример, что и раньше, но первое, что необходимо заметить,— здесь нет добавленных в класс Inventory атрибутов. Поэтому в данном случае представьте, что классы Inventory, Product и производный класс BookProduct находятся в отдельной DLL, и у нас нет исходного кода.

Первым шагом в процессе является создание объекта на основе XmlAttributes, и объекта XmlElementAttribute для каждого типа данных, который будет переопределяться:

XmlAttributes attrs=new XmlAttributes();

attrs.XmlElements.Add(new XmlElementAttribute("Book", typeof(BookProduct)));

attrs.XmlElements.Add(new XmlElementAttribute("Product", typeof(Product)));

Здесь мы добавляем новый XmlElementAttribute к коллекции XmlElements класса XmlAttributes. Класс XmlAttributes имеет свойства, соответствующие атрибутам, которые могут применяться; XmlArray и XmlArrayItems, которые мы видели в предыдущем примере, являются только парой. Теперь мы имеем объект XmlAttributes с двумя объектами на основе XmlElementAttribute, добавленными к коллекции XmlElements. Далее создадим объект XmlAttributeOverrides:

XmlAttributeOverrides attrOver = new XmlAttributeOverride();

attrOver.Add(typeof(Inventory) , "Inventory Items", attrs);

Meтод Add имеет две перегружаемые версии. Первая получает информацию о типе переопределяемого объекта и объект XmlAttributes, который был создан ранее. Вторая версия та, что мы используем, получает также с строковое значение, которое является членом в переопределенном объекте. В нашем случае мы хотим переопределить член InventoryItems в классе Inventory.

Теперь создадим объект XmlSerializer, добавляя объект XmlAttributeOverridesв качестве параметра. XmlSerializer уже знает, какие типы мы хотим переопределить и что нам нужно вернуть для этих типов. Если выполнить метод Serialize, то получится следующий вывод XML:

<?xml version="1.0"?>

<Inventory xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">

<Products>

<ProductID>100</ProductID>

<ProductName>Product Thing</ProductName>

<SupplierID>10</SupplierID>

</Product>

<Book>

<ProductID>101</ProductID>

<ProductName>How to Use Your New Product Thing</ProductName>

<SupplierID>10</SupplierID>

<ISBN>123456789</ISBN>

</Book>

</Inventory>

Мы получили тот же самый XML, что и в предыдущем примере. Чтобы десериализовать этот объект и воссоздать объект на основе Inventory, с которого мы начали, необходимо создать все те же объекты XmlAttributes, XmlElementAttribute и XmlAttributeOverrides, которые создаются при сериализации объекта. Когда это будет сделано, можно прочитать XML и воссоздать объект Inventory, как это делалось раньше. Вот код для десериализации объекта Inventory:

private void button2_Click(object sender, System.EventArgs e) {

XmlAttributes attrs=new XmlAttributes();

attrs.XmlElements.Add(new XmlElementAttribute("Book", typeof(BookProduct)));

attrs.XmlElements.Add(new XmlElementAttribute("Product", typeof(Product)));

XmlAttributeOverrides attrOver=new XmlAttributeOverrides();

attrOver.Add(typeof(Inventory), "InventoryItems", attrs);

Inventory newInv;

FileStream f=new FileStream("..\\..\\..\\inventory.xml", FileMode.Open);

XmlSerializer newSr=new XmlSerializer(typeof(Inventory), attrOver);

newInv=(Inventory)newSr.Deserialize(f);

foreach(Product prod, in newInv.InventoryItems) listBox1.items.Add(prod.ProductName);

f.Close();

}

Отметим, что первые несколько строк кода идентичны коду, который использовался для сериализации объекта.

Пространство имен XmlSerialize предоставляет мощный набор инструментов. Сериализуя объекты в XML, а не в двоичный формат, мы получаем дополнительные возможности, что может действительно увеличить гибкость проектирования.

Заключение

В этой главе рассматривались широкие возможности пространства имен System.Xml платформы .NET. Было показано, как прочитать и записать документы XML с помощью классов на основе XMLReader и XMLWriter, как в .NET реализована DOM и как использовать возможности DOM. Мы увидели, что XML и ADO.NET действительно очень тесно связаны. DataSet и документ XML являются двумя различными представлениями одной и той же базовой архитектуры. Мы сериализовали объекты в XML и смогли вернуть их обратно с помощью вызова пары методов. Комбинация Reflection и XMLSerilization приводит к некоторым уникальным конструкциям. И, конечно, были рассмотрены XPath и XslTransform. В течение ближайших нескольких лет XML станет, если уже не стал, важной частью разработки приложений. Платформа .NET сделала доступным мощный набор инструментов для работы с XML.

Глава 14

Операции с файлами и реестром

В этой главе будет рассмотрено выполнение в C# задач, включающих чтение и запись в файлы и в системный реестр. В частности, будут охвачены следующие вопросы:

□ Исследование структуры каталога, выяснение, какие файлы и папки присутствуют и проверка их свойств

□ Перемещение, копирование и удаление файлов и папок

□ Чтение и запись текста в и из файлов

□ Чтение и запись в реестр

Компания Microsoft предоставила очень интуитивную объектную модель, охватывающую эти области, и в ходе этой главы мы покажем, как использовать классы на основе .NET для выполнения упомянутых выше задач. Для случая операций с файловой системой соответствующие классы почти все находятся в пространстве имен System.IO, в то время как операции с реестром связаны с парой классов в пространстве имен System.Win32.

Классы на основе .NET включают также ряд классов и интерфейсов в пространстве имен System.Runtime.Serilization, которые связаны с сериализацией, то есть процессом преобразования некоторых данных (например, содержимого документа) в поток байтов для хранения в каком либо месте. Мы не будем рассматривать эти классы в данной главе, так как сосредоточимся на классах, которые предназначены для прямого доступа к файлам.

Отметим, что хотя безопасность влияет на все области, но особенно она важна при модификации файлов или записей реестра. Система безопасности рассматривается отдельно в главе 25. В этой главе мы будем предполагать, что имеются достаточные права доступа для выполнения всех примеров, которые модифицируют файлы или записи реестра, что обеспечивается, например, при использовании учетной записи с полномочиями администратора.

Управление файловой системой

Классы, которые используются для просмотра файловой системы и выполнения таких операций, как перемещение, копирование и удаление файлов, показаны на следующей диаграмме. Пространство имен каждого класса показано в скобках под именем каждого класса на диаграмме:

Назначение этих классов следующее:

□ System.MarshalByRefObject — класс базового объекта для классов .NET, которые являются удаленными. Допускает маршализацию данных между прикладными доменами.

□ FileSystemInfo — базовый класс, который представляет любой объект файловой системы.

□ FileInfo и File — представляют файл в файловой системе.

□ DirectoryInfo и Directory — представляют папку в файловой системе.

□ Path — содержит статические члены, которые можно использовать для манипуляций именами путей доступа.

Отметим, что в Windows объекты, которые содержат файлы и используются для организации файловой системы, называются папками. Например, в пути доступа C:\My Documents\ReadMe.txt файлом является ReadMe.txt, а My Documents — папкой. Папка (folder) является специфическим для Windows терминам: практически во всех других операционных системах вместо папки используется термин каталог (directory). В соответствии с желанием Microsoft, чтобы .NET была максимально независимой от операционной системы, соответствующие базовые классы .NET называются Directory и DirectoryInfo. Однако в связи с возможной путаницей с каталогами LDAP (обсуждаемыми в главе 15), и в связи с тем, что эта книга посвящена Windows, здесь используется термин папка.

Классы .NET, представляющие файлы и папки

Из приведенного выше списка можно видеть, что существуют два класса, используемых для представления папки, и два класса для файла. Какой из этих классов будет наиболее подходящим, зависит в большой степени от того, сколько раз необходимо получать доступ к папке или файлу:

□ Directory и File содержат только статические методы и никогда не создают экземпляров. Эти классы применяются с помощью предоставления пути доступа к соответствующему объекту файловой системы, когда вызывается метод-член. Если нужно выполнить только одну операцию на папке или файле, то эти классы более эффективны, так как не требуют создания экземпляра класса .NET.

□ DirectoryInfo и FileInfo реализуют приблизительно те же открытые методы, что и Directory и File, а также некоторые открытые свойства и конструкторы, но они имеют состояние и члены этих классов не являются статическими. На самом деле необходимо создать экземпляры этих классов, и при этом каждый экземпляр ассоциируется с определенной папкой или файлом. Это означает, что эти классы являются более эффективными, если выполняется несколько операций с помощью одного и того же объекта, так как они будут считывать аутентификационные и другие данные при создании соответствующего объекта файловой системы, а затем им не понадобиться больше считывать эту информацию, независимо от того, сколько методов вы будете вызывать на каждом объекте (экземпляре класса). Для сравнения, соответствующим классам без состояния понадобиться снова проверять данные файла или папки для каждого вызываемого метода.

В этом разделе мы будем в основном использовать классы FileInfo и DirectoryInfo, но оказывается, что многие (но не все) вызываемые методы реализуются также классами File и Directory (хотя в этих случаях эти методы требуют дополнительный параметр, имя пути доступа объекта файловой системы и пара методов имеют немного отличные имена). Например:

FileInfo MyFile = new FileInfo(@"C:\Program Files\My Program\ReadMe.txt");

MyFile.CopyTo(@"D:\Copies\ReadMe.txt");

Имеет тот же результат, что и:

File.Copy(@"C:\Program Files\My Program\ReadMe.txt", @"D:\Copies\ReadMe.txt");

Первый фрагмент кода будет выполняться немного дольше, так как он требует создания экземпляра объекта FileInfo — MyFile, но он оставляет MyFile готовым для выполнения дальнейших действий на том же файле.

Для создания экземпляра класса FileInfo или DirectoryInfo в конструктор передается строка, содержащая путь доступа к соответствующей файловой системе. Мы только что проиллюстрировали процесс для файла. Для папки код выглядит аналогично:

DirectoryInfo MyFolder = new DirectoryInfo(@"C:\Program Files");

Если путь доступа представляет объект, который не существует, то исключение будет порождено не во время создания, а в тот момент, когда будет вызван метод, которому потребовался объект соответствующей файловой системы. Можно определить существует ли объект и имеет ли соответствующий тип, проверяя свойство Exists, которое реализовано для обоих этих классов:

FileInfo Test = new FileInfo(@"C:\Windows");

Console.WriteLine(Test.Exists.ToString());

Console.WriteLine(Test.CreationTime.ToString());

Отметим, что для того, чтобы это свойство возвращало true, соответствующий объект файловой системы должен быть соответствующего типа. Другими словами, если создается экземпляр объекта FileInfo, содержащий путь доступа папки или, если создается объект DirectoryInfo, задающий путь доступа файла, Exists будет иметь значение false. С другой стороны, большинство свойств и методов этих объектов будут возвращать значение, если вообще это возможно. Но они не обязательно порождают исключение из-за того, что был вызван неправильный тип объекта, а только в том случае, если требовалось выполнить что-то реально невозможное. Например, приведенный выше фрагмент кода сначала выведет false (так как C:\Windows является папкой), но затем все равно правильно покажет время создания папки, так как в папке имеется эта информация. С другой стороны, если затем попробовать открыть папку, как если бы это был файл, с помощью метода FileInfo.Open(), то будет порождено исключение.

После того, как определено, что соответствующий объект файловой системы существует, можно (если используется класс FileInfo или DirectoryInfo) найти о нем информацию, используя ряд свойств, включающих:

Имя

Назначение

CreationTime

Время создания файла или папки.

DirectoryName(FileInfo), Parent(DirectoryInfo)

Полный путь доступа содержащей папки.

Exists

Существует ли файл или папка.

Extension

Расширение файла. Возвращается пустым для папок.

FullName

Полное имя пути доступа файла или папки.

LastAccessTime

Время последнего доступа к файлу или папке.

LastWriteTime

Время последней модификации файла или папки.

Name

Имя файла или папки.

Root

(Только DirectoryInfo.) Корневая часть пути доступа.

Length

(Только FileInfo.) Возвращает размер файла в байтах.

Можно также выполнить действия на объекте файловой системы с помощью следующих методов:

Имя

Назначение

Create()

Создает папку или пустой файл с заданным именем. Для FileInfo он возвращает также объект потока, чтобы позволить записать в файл. Потоки будут рассмотрены позже.

Delete()

Удаляет файл или папку. Для папок существует вариант рекурсивного метода Delete.

MoveTo()

Перемещает и/или переименовывает файл или папку.

CopyTo()

(Только FileInfo.) Копирует файл. Отметим, что не существует метода копирования для папок. Если копируются все деревья каталогов, то необходимо индивидуально скопировать каждый файл и создать новые папки, соответствующие старым папкам.

GetDirectories()

(Только DirectoryInfo.) Возвращает массив объектов DirectoryInfo, представляющих все папки, содержащиеся в этой папке.

GetFiles()

(Только DirectoryInfo.) Возвращает массив объектов FileInfo, представляющих все папки, содержащиеся в этой папке.

GetFileSystemObjects()

(Только DirectoryInfo.) Возвращает объекты FileInfo и DirectoryInfo, представляющие все объекты, содержащиеся в этой папке, как массив ссылок FileSystemInfo.

Отметим, что приведенные выше таблицы показывают основные свойства и методы, и не являются исчерпывающими.

В приведенных выше таблицах не перечислены большинство свойств или методов, которые позволяют записывать или читать данные в файлах. Это в действительности делается с помощью потоковых объектов, которые будут рассмотрены позже. FileInfo реализует также ряд методов (Open(), OpenRead(), OpenText(), OpenWrite(), Create(), CreateText(), которые возвращают объекты потоков для этой цели).

Интересно то, что время создания, время последнего доступа, и время последней записи являются изменяемыми:

// Test является FileInfo или DirectoryInfo. Задать время создания

// как 1 Jan 2001, 7.30 am

Test.CreationTime = new DateTime(2001, 1, 1, 7, 30, 0);

Это может показаться странным, но на самом деле достаточно полезно. Например, если имеется программа, которая эффективно модифицирует файл, просто считывая его, затем удаляя его и создавая новый файл с новым содержимым, то будет желательно изменить дату создания, чтобы противопоставить первоначальной дате создания старого файла.

Класс Path

Класс Path не является классом, экземпляры которого будут создаваться. Скорее он предоставляет некоторые статические методы, которые облегчают работу с путями доступа. Например, предположим, что необходимо вывести имя полного пути доступа для файла ReadMe.txt в папке C:\My Documents. Путь доступа к файлу можно найти с помощью следующей операции:

Console.WriteLine(Path.Combine(@"C:\My Documents", "ReadMe.txt"));

Использовать класс Path значительно проще, чем пытаться справиться с символами-разделителями вручную, потому что класс Path знает различные форматы имен путей доступа в различных операционных системах. Во время написания книги Windows являлась единственной операционной системой, поддерживаемой .NET, но если, например, .NET будет в дальнейшем перенесена на Unix, то Path сможет справиться с путями доступа Unix, где в качестве разделителя в именах путей доступа используется /, а не \. Path.Combine является методом этого класса, который будет вероятно использоваться чаще всего, но Path реализует также другие методы, которые предоставляют информацию о пути доступа или требуемом для него формате.

В следующем разделе будет дан пример, который поясняет, как просмотреть каталоги и увидеть свойства файлов

Пример: файловый браузер

В этом разделе представлен пример приложения C#, называемого FileProperties. FileProperties представляет простой интерфейс пользователя, который позволяет перемещаться по файловой системе и видеть время создания время последнего доступа, время последней записи и размер файлов.

Приложение FileProperties выглядит следующим образом. Пользователь вводит имя папки или файла в основное текстовое поле в верхней части окна и нажимает кнопку Display. Если вводится путь доступа к папке, ее содержимое выводится в окне списка. Если ввести путь доступа к файлу, то данные о нем появятся в текстовых полях в нижней части формы, а содержимое папки — в окне списка. На экране показано приложение FileProperties, используемое для просмотра папки:

Пользователь может очень легко перемещаться по файловой системе, щелкая мышью на любой из папок в правом окне списка для перехода в эту папку или на кнопке Up для перемещения в родительскую папку. На приведенном выше экране в основном текстовом поле введено C:\4990, чтобы получить содержимое этого раздела, который использует окно списка для дальнейшего перемещения. Пользователь может также выбрать файл, щелкая на его имени в окне списка, в этом случае его свойства выводятся в текстовых полях:

Обратите внимание, что при желании можно также вывести время создания, время последнего доступа и время последнего изменения для папок — DirectoryInfo реализует соответствующие свойства. Мы выводим эти свойства только для выбранного файла, чтобы сохранить простоту приложения.

Мы создаем проект как стандартное приложение C# Windows в Visual Studio.NET и добавляем различные текстовые поля и окно списка из области Windows Forms в панели инструментов. Затем элементы управления переименовываются в более понятные имена txtBoxInput, txtBoxFolder, buttonDisplay, buttonUp, listBoxFiles, listBoxFolders, textBoxFileName, textBoxCreationTime, txtBoxLastAccessTime, txtBoxLasrWriteTime и textBoxFileSize.

После чего добавляется следующий код:

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.IO;

Нам необходимо делать это для всех примеров в этой главе, связанных с файловой системой, но мы не будем явно показывать этот код в остальных примерах. Затем к основной форме добавляется поле-член:

public class Form1 : System.Windows.Forms.Form {

#region Member Fields

private string currentFolderPath;

currentFolderPath будет содержать путь доступа папки, содержимое которой будет выводится в данный момент в окне списка.

Теперь нам нужно добавить обработку генерируемых пользователем событий. При этом возможны следующие способы вывода:

□ Пользователь щелкает на кнопке Display. В этом случае необходимо определить, что введенный пользователем текст в основном текстовом поле является путем доступа к файлу или папке. Если это папка, мы перечисляем файлы и вложенные папки этой папки в окне списка. Если это файл, мы делаем это для папки, содержащей этот файл, но также выводим свойства файла в нижних текстовых полях.

□ Пользователь щелкает на имени файла в окне списка Files. В этом случае мы выводим свойства этого файла в нижних текстовых полях.

□ Пользователь щелкает на имени папки в окне списка Folders. В этом случае мы очищаем все элементы управления и затем выводим содержимое этой вложенной папки в окне списка.

□ Пользователь щелкает на кнопке Up. В этом случае мы очищаем все элементы управления и затем выводим содержимое родительской папки в окне списка.

Прежде чем показать код обработчиков событий, приведем код методов. Для начала необходимо очистить содержимое всех элементов управления. Этот метод вполне очевиден:

protected void ClearAllFields() {

listBoxFolders.Items.Clear();

listBoxFiles.Items.Clear();

txtBoxFolder.Text = "";

txtBoxFileName.Text = "";

txtBoxCreationTime.Text = "";

txtBoxLastAccessTime.Text = "";

txtBoxLastWriteTime.Text = "";

txtBoxSize.Text ="";

}

Затем определяется метод DisplayFileInfo(), который обрабатывает процесс вывода информации для заданного файла в текстовых полях. Этот метод получает один параметр, полное имя пути доступа файла и работает, создавая объект FileInfo на основе этого пути доступа:

protected void DisplayFileInfo(string fileFullName) {

txtBoxFileName.Text = TheFile.Name;

txtBoxCreationTime.Text = TheFile.CreationTime.ToLongTimeString();

txtBoxLastAccessTime.Text = TheFile.LastAccessTime.ToLongDateString();

txtBoxLastWriteTime.Text = TheFile.LastWriteTime.ToLongDateString();

txtBoxFileSize.Text = TheFile.Length.ToString() + " bytes";

}

Отметим, что здесь мы воздержались от порождения исключения в случае каких-либо проблем с местоположением файла. Исключение будет обрабатываться в вызывающей процедуре (в одном из обработчиков событий). Наконец, мы определяем метод DisplayFolderList(), который выводит содержимое данной папки в двух окнах списка. Полное имя пути доступа папки передается в этот метод в качестве параметра:

protected void DisplayFolderList(string folderFullName) {

DirectoryInfo TheFolder = new DirectoryInfo(folderFullName);

if (TheFolder.Exists)

throw new DirectoryNotFoundException("Folder not found: " + folderFullName);

ClearAllFields();

txtBoxFolder.Text = TheFolder.FullName;

currentFolderPath = TheFolder.FullName;

// перечисляем все папки, вложенные в папку

foreach(DirectoryInfo NextFolder in TheFolder.GetDirectories())

listBoxFolders.Items.Add(NextFolder.Name);

// перечисляем все файлы в папке

foreach (FileInfo NextFile in TheFolder.GetFiles())

listBoxFiles.Items.Add(NextFile.Name);

}

Теперь мы проверим программы обработки событий. Обработчик события нажатия на кнопку Display является самым сложным, так как ему необходимо обработать три различные возможности для вводимого пользователем текста. Это может быть имя пути доступа папки, имя пути доступа файла или ни то, ни другое:

protected void onDisplayButtonClick(object sender, EventArgs e) {

try {

string FolderPath = txtBoxInput.Text;

DirectoryInfo TheFolder = new DirectoryInfo(FolderPath);

if (TheFolder.Exists) {

DisplayFolderList(TheFolder.FullName);

return;

}

FileInfo TheFile = new FileInfo(FolderPath);

if (TheFile.Exists) {

DisplayFolderList(TheFile.Directory.FullName);

int Index = listBoxFiles.Items.IndexOf(TheFile.Name);

listBoxFiles.SetSelected(Index, true);

return;

}

throws

new FileNotFoundException("There is no file or folder with " + "this name: " + txtBoxInput.Text);

} catch (Exception ex) {

MessageBox.Show(ex.Message);

}

}

В приведенном выше коде мы определяем, что поставляемый текст представляет папку или файл, создавая по очереди экземпляры DirectoryInfo и FileInfo и проверяя свойство Exists каждого объекта. Если ни один из них не существует, то порождается исключение. Если это папка, вызывается метод DisplayFolderList, чтобы заполнить окна списков. Если это файл, то необходимо заполнить окна списков и текстовые поля, которые выводят свойства файла. Мы обрабатываем этот случай, заполняя сначала окна списков. Затем программным путем выбирается соответствующее имя файла в окне списка файлов. Это имеет в точности тот же результат, как если бы пользователь выбрал этот элемент, порождается событие выбора элемента. Затем можно просто выйти из текущего обработчика событий, зная, что обработчик событий выбранного элемента будет немедленно вызван для вывода свойств файла.

Следующий код является обработчиком событий, который вызывается, когда в окне списка выбирается элемент либо пользователем, либо, как указано выше, программным путем. Он создает полное имя пути доступа выбранного файла и передает его в метод DisplayFileInfo(), который был показан ранее:

protected void OnListBoxFilesSelected(object sender, EventArgs e) {

try {

string SelectedString = listBoxFiles.SelectedItem.ToString();

string FullFileName = Path.Combine(currentFolderPath, SelectedString);

DisplayFileInfo(FullFileName);

} catch (Exception ex) {

MessageBox(ex.Message);

}

}

Обработчик событий выбора папки в окне списка папок реализуется похожим образом, за исключением того, что в этом случае вызывается DisplayFolderList() для обновления содержимого окон списков:

protected void OnListBoxFoldersSeleeted(object sender, EventArgs e) {

try {

string SelectedString = listBoxFolders.SelectedItem.ToString();

string FullPathName = Path.Combine(currentFolderPath, SelectedString);

Display FolderList(FullPathName);

} catch (Exception ex) {

MessageBox.Show(ex.Message);

}

}

Наконец, когда происходит щелчок на кнопке Up, должен также вызываться метод DisplayFolderList(), за исключением того, что в этот раз необходимо получить путь доступа предка выводимой в данный момент папки. Это достигается с помощью свойства FileInfo.DirectoryName, которое возвращает путь доступа родительской папки

protected void OnUpButtonClick(object sender, EventArgs e) {

try {

string FolderPath = new FileInfo(currentFolderPath).DirectoryName;

DisplayFolderList(FolderPath);

} catch (Exception ex) {

MessageBox(ex.Message);

}

}

Отметим, что для этого проекта мы не показали код, который добавляет методы обработки событий к соответствующим событиям элементов управления. Нам не нужно добавлять этот код вручную, так как согласно замечанию в главе 7 можно использовать окно Properties в Visual Studio для связывания каждого метода обработки событий с событием.

Перемещение, копирование и удаление файлов

Мы уже упоминали, что перемещение и удаление файлов или папок делается методами MoveTo() и Delete() классов FileInfo и DirectoryInfo. Эквивалентными методами в классах File и Directory являются Move() и Delete(). Классы FileInfo и File также соответственно реализуют методы CopyTo() и Copy(). Однако не существует методов для копирования полных папок, необходимо сделать это, копируя каждый файл в папке.

Использование всех этих методов является вполне понятным, все детали можно найти в MSDN. В этом разделе мы проиллюстрируем их использование для определенных случаев вызова статических методов Move(), Copy() и Delete() на классе File. Чтобы сделать это, мы преобразуем предыдущий пример FileProperties в новый пример FilePropertiesAndMovement. Этот пример будет иметь дополнительное свойство, позволяющее при выводе свойств файла выполнить удаление этого файла или перемещение или копирование его в другое место.

Пример FilePropertiesAndMovement

Пример выглядит следующим образом:

На этом экране можно видеть, что он похож внешне на пример FileProperties, за исключением того, что существует дополнительная группа из трех кнопок и текстового поля в нижней части окна. Эти элементы управления будут доступны только в том случае, когда пример действительно выводит свойства файла, в других случаях они будут отключены. Мы поджали существующие элементы управления немного вверх, чтобы основная форма не становилась слишком большой. Когда выводятся свойства файла, пример автоматически помещает полное имя пути доступа этого файла в нижнем текстовом поле для редактирования пользователю. Пользователь может затем щелкнуть по любой из кнопок, чтобы выполнить соответствующую операцию. Когда это делается, появляется окно с сообщением, подтверждающем действие. В приведенном выше случае, если пользователь щелкает на CopyTo, мы увидим следующее сообщение:

Если действие подтверждается, он будет двигаться дальше. При перемещении или удалении файла мы, очевидно, не сможем продолжать выводить содержимое этого файла из того же места. При копировании файла с другим именем файла в ту же папку изображение также будет устаревшим. Во всех этих случаях пример сбрасывает свои элементы управления для вывода послефайловой операции, содержащей только папки.

Чтобы закодировать это, необходимо добавить подходящие элементы управления, а также их методы обработки событий к коду примера FileProperties. Мы задаем для новых элементов управления имена buttonDelete, buttonCopyTo, buttonMoveTo, и txtBoxNewPath.

Посмотрим сначала на метод обработки событий, который вызывается, когда пользователь нажимает кнопку Delete:

protected void OnDeleteButtonClick(object sender, EventArgs e) {

try {

string FilePath = Path.Combine(currentFolderPath, txtBoxFileName.Text);

string Query = "Really delete the file\n" + FilePath + "?";

if (MessageBox.Show(Query, "Delete File?", MessageBoxButtons.YesNo) == DialogResult.Yes) {

File.Delete(FilePath); DisplayFolderList(currentFolderPath);

}

} catch (Exception ex) {

MessageBox.Show("Unable to delete file. The following exception" + " occured:\n" + ex.Message, "Failed");

}

}

Код этого метода находится в блоке try в связи с очевидным риском порождения исключения, если, например, у нас нет полномочий на удаление файла или файл был перемещен другим процессом за время, прошедшее с момента вывода данных примером, и моментом, когда пользователь нажал кнопку удаления. Мы создаем путь доступа файла, чтобы удалить его из поля CurrentParentPath, которое будет содержать путь доступа текущей папки, и формируем текст в поле textBoxFileName, которое будет содержать имя файла.

Методы для перемещения и копирования файла структурированы похожим образом:

protected void OnMoveButtonClickfobject sender, EventArgs e) {

try {

string FilePath = Path.Combine(currentFolderPath, txtBoxFileName.Text);

string Query =

"Really move the file\n" + FilePath + "\nto " + txtBoxNewPath.Text + "?";

if (MessageBox.Show(Query, "Move File?", MessageBoxButtons.YesNo) == DialogResult.Yes) {

File.Move(FilePath, txtBoxNewPath.Text);

DisplayFolderList(currentFolderPath);

}

} catch(Exception ex) {

MessageBox.Show("Unable to move file. The following exception" + " occured: \n" + ex.Message, "Failed");

}

}

protected void OnCopyButtonClick(object sender, EventArgs e) {

try {

string FilePath = Path.Combine(currentFolderPath, txt.BoxFileName.Text);

string Query = "Really copy the file\n" + FilePath + "\nto " + txtBoxNewPath.Text + "?";

if (MessageBox.Show(Query, "Copy File?", MessageBoxButtons.YesNo) == DialogResult.Yes) {

File.Copy(FilePath, txtBoxNewPath.Text);

DisplayFolderList(currentFolderPath);

}

} catch (Exception ex) {

MessageBox.Show("Unable to copy file. The following exception" + " occured:\n" + ex.Message, "Failed");

}

}

Нам нужно также убедиться в том, что новые кнопки и текстовое поле включаются и отключаются в соответствующее время. Чтобы включить их, когда выводится содержимое файла, мы добавляем следующий код в DisplayFileInfo():

protected void DisplayFileInfo(string fileFullName) {

FileInfo TheFile = new FileInfo(fileFullName);

if (!TheFile.Exists) throw new FileNotFoundException("File not found: " + fileFullName);

txtBoxFileName.Text = TheFile.Name;

txtBoxCreationTime.Text = TheFile.CreationTime.ToLongTimeString();

txtBoxLastAccessTime.Text = TheFile.LastAccessTime.ToLongDateString();

txtBoxLastWriteTime.Text = TheFile.LastWriteTime.ToLongDateString();

txtBoxFileSize.Text = TheFile.Length.ToString() + " bytes";

// включает кнопки перемещения, копирования и удаления

txtBoxNewPath.Text = TheFile.FullName;

txtBoxNewPath.Enabled = true;

buttonCopyTo.Enabled = true;

buttonDelete.Enabled = true;

buttonMoveTo.Enabled = true;

}

Нам нужно также сделать одно изменение в DisplayFolderInfo:

protected void DisplayFolderList(string folderFullName) {

DirectoryInfo TheFolder = new DirectoryInfo(folderFullName);

if (!TheFolder.Exists)

throw new DirectoryNotFoundException("Folder not found: " + folderFullName);

ClearAllFields();

DisableMoveFeatures();

txtBoxFolder.Text = TheFolder.FullName;

currentFolderPath = TheFolder.FullName;

// перечислить все папки, вложенные в папку

foreach(DirectoryInfo NextFolder in TheFolder.GetDirectories())

listBoxFolders.Items.Add(NextFolder.Name);

// перечислить все файлы в папке

foreach (FileInfo NextFile in TheFolder.GetFiles())

listBoxFiles.Items.Add(NextFile.Name);

}

DisableMoveFeatures является небольшой служебной функцией, которая отключает новые элементы управления:

void DisableMoveFeatures() {

txtBoxNewPath.Text = "";

txtBoxNewPath.Enabled = false;

buttonCopyTo.Enabled = false;

buttonDelete.Enabled = false;

buttonMoveTo.Enabled = false;

}

Нам также понадобится добавить код в ClearAllFields(), чтобы очистить дополнительное текстовое поле:

protected void ClearAllFields() {

listBoxFolders.Items.Clear();

listBoxFiles.Items.Clear();

txtBoxFolder.Text = "";

txtBoxFileName.Text = "";

txtBoxCreationTime.Text = "";

txtBoxLastAccessTime.Text = "";

txtBoxLastWriteTime.Text = "";

txtBoxFileSize.Text = "";

txtBoxNewPath.Text = "";

}

После этого код закончен.

Чтение и запись файлов

Чтение и запись файлов является в принципе очень простым процессом, но делается это не с помощью объектов DirectoryInfo или FileInfo, которые только что были рассмотрены. Вместо этого используется ряд классов, которые представляют общую концепцию, называемую потоком.

Потоки

Идея потока существует уже очень давно. Поток является объектом, используемым для пересылки данных. Данные могут передаваться в одном или в двух направлениях:

□ Если данные передаются в программу из некоторого внешнего источника, то речь идет о чтении из потока.

□ Если данные передаются из программы в некоторый внешний источник, то речь идет о записи в поток.

Очень часто внешний источник является файлом, но не всегда. Другими вариантами могут быть:

□ Чтение или запись данных в сети с помощью некоторого сетевого протокола, куда посылают данные или получают с другого компьютера.

□ Чтение или запись через именованный канал.

□ Чтение или запись данных в области памяти.

Для таких примеров Microsoft поставляет базовый класс .NET для записи в память и чтения из памяти System.IO.MemoryStream, в то время как System.Net.Sockets.Networkstream обрабатывает сетевые данные. Не существует базовых классов потока для записи в каналы или чтения из каналов, но существует базовый класс потока, System.IO.Stream, из которого можно создать, если понадобиться, производный класс. Поток не делает никаких предположений о природе внешнего источника данных.

Внешний источник иногда бывает даже переменной в коде приложения. Возможно, это звучит парадоксально, но техника использования потоков для передачи данных между переменными может оказаться полезным приемом для преобразования типов данных. Язык С использовал что-то подобное для преобразования между целыми типами данных и строками или для форматирования строк с помощью функции sprintf(), а в C# два базовых класса .NET, StringReader и StringWriter, могут использоваться в таком контексте.

Преимущество применения отдельного объекта для передачи данных, вместо классов FileInfo и DirectoryInfo, состоит в том, что разделение концепции передачи данных и определенного источника данных облегчает замену источников данных. Сами объекты потоков содержат большой объем базового кода, имеющего отношение к переносу данных между внешними источниками и переменными в коде приложения, и сохраняя этот код отдельно от любой концепции определенного источника данных, мы облегчаем повторное применения этого кода (через наследование) в различных обстоятельствах. Например, упомянутые выше классы StringReader и StringWriter являются частью того же дерева наследования, что и два класса, используемых для чтения и записи текстовых файлов, — StreamReader и StreamWriter. Классы почти наверняка неявно задействуют значительный объем общего кода.

Реальная иерархия связанных с потоком классов в пространстве имен System.IO выглядит следующим образом:

Что касается чтения из файлов или записи в файлы, то мы будем связаны в основном со следующими классами:

□ FileStream. Этот класс предназначен для чтения и записи двоичных данных в произвольный двоичный файл, однако при желании можно использовать его для чтения и записи в любой файл.

□ StreamReader и StreamWriter. Эти классы специально предназначены для чтения и записи в текстовые файлы.

Упомянем также другие классы, которые могут оказаться полезными, хотя они и не будут представлены в приводимых примерах. Если вы хотите использовать эти классы, обратитесь к документации MSDN, чтобы получить подробности об их работе.

BinaryReader и BinaryWriter. Эти классы в действительности сами не реализуют потоки, но они могут обеспечить оболочки вокруг других потоковых объектов. BinaryReader и BinaryWriter поддерживают дополнительное форматирование двоичных данных, что позволяет напрямую читать или записывать содержимое переменных C# в соответствующий поток. Проще всего считать, что BinaryReader и BinaryWriter находятся между потоком и кодом приложения, обеспечивая дополнительное форматирование:

Различие между использованием этих классов и непосредственным использованием описанных ниже потоковых объектов состоит в том, что базовый поток работает с байтами. Например, пусть часть процесса сохранения некоторого документа состоит в записи содержимого переменной типа long в двоичный файл. Каждая переменная типа long занимает 8 байтов, если используется плоский обыкновенный двоичный поток, необходимо будет явно записывать каждые эти 8 байтов памяти. В коде C# это будет означать, что необходимо явно выполнять некоторые битовые операции для извлечения каждых 8 байтов из значения long. Используя экземпляр BinaryWriter, можно инкапсулировать всю операцию в перегруженный метод BinaryWriter.Write(), который получает long в качестве параметра и который будет помещать эти 8 байтов в поток (и следовательно, если поток направлен в файл, то в файл). Соответствующий метод BinaryReader.Read() будет извлекать 8 байтов из потока и восстанавливать значение long.

BufferedStream. По соображениям производительности при чтении из файла или при записи в файл вывод буферизуется. Это означает, что если программа запрашивает следующие 2 байта файлового потока и поток передает запрос Windows, то Windows не станет соединяться с файловой системой и затем искать и считывать файл с диска, для того чтобы получить 2 байта. Вместо этого произойдет извлечение большого блока файла за один раз и сохранение этого блока в области памяти, называемой буфером. Последующие запросы данных из потока будут удовлетворяться из буфера, пока он не будет исчерпан, и тогда Windows извлечет другой блок данных из файла. Запись в файлы работает таким же образом. Для файлов это делается автоматически операционной системой, но возможен случай, когда придется написать потоковый класс для чтения из некоторого другого устройства, которое не буферизуется. В таком случае можно вывести этот класс из BufferedStream, который сам реализует буфер (BufferedStream не создан, однако, для ситуаций, когда приложение часто чередует операции чтения и записи данных).

Чтение и запись двоичных файлов

Чтение и запись двоичных файлов делается обычно с помощью класса FileStream.

Класс FileStream

Экземпляр FileStream используется для чтения или записи данных файла. Чтобы создать FileStream, необходимо иметь данные четырех видов:

□ Файл для доступа.

□ Режим, который указывает, как необходимо открыть файл. Например, собираетесь ли вы создать новый файл или открыть существующий файл, и, если открывается существующий файл, должна ли какая-либо операция записи интерпретироваться как перезапись содержимого файла или как добавление к файлу.

□ Доступ, указывающий, как будет осуществляться доступ к файлу, будет ли выполняться чтение или запись в файл или и то и другое.

□ Общий доступ. Другими словами, будет ли осуществляться исключительный доступ к файлу, или желательно, чтобы другие потоки могли одновременно получать доступ к этому файлу. Если так, то должны ли другие потоки иметь доступ для чтения файла, записи в него или для того и другого.

Первый из этих видов данных представлен обычно строкой, которая содержит полное имя пути доступа файла, и в этой главе будут рассматриваться только те конструкторы, которые требуют строку. Помимо этих конструкторов, существуют и некоторые другие, которые получают дескриптор файла Windows в стиле старого Windows API. Остальные три вида данных представлены тремя перечислениями .NET, называемыми соответственно FileMode, FileAccess и FileShare. Значения этих перечислений должны быть понятны из названий.

Перечисление

Значения

FileMode (режим файла)

Append (добавить), Create (создать), CreateNew (создать новый), Open (открыть), OpenOrCreate (открыть или создать), Truncate (обрезать)

FileAccess (доступ к файлу)

Read (чтение), ReadWrite (чтение-запись), Write (запись)

FileShare (общий доступ к файлу)

None (нет), Read (чтение), ReadWrite (чтение-запись), Write (запись)

Отметим, что в случае FileMode могут порождаться исключения, если запросить режим, который несогласован с существующим статусом файла. Append, Open и Truncate будут порождать исключение, если файл еще не существует, a CreateNew будет порождать исключение, если он существует. Create и OpenOrCreate будут удовлетворять любому сценарию, но Create будет удалять любой существующий файл для замены его новым, вначале пустым файлом.

Существует большое число конструкторов для FileSream. Три простейшие из них работают следующим образом:

// создает файл с доступом read-write и предоставляет другим потокам

// доступ на чтение

FileStream fs = new FileStream(@"C:\C# Projects\Projects.doc", FileMode.Create);

// как и выше, но мы получаем доступ к файлу только на запись

FileStream fs2 = new FileStream(@"C:\C# Projects\Projects2.doc", FileMode.Create, FileAccess.Write);

// как и выше, но другие потоки не имеют никакого доступа к файлу,

// пока fs3 открыт

FileStream fs3 = new FileStream(@"C:\C# Projects\Projects3.doc", FileMode.Create, FileAccess.Read, FileShare.None);

Из этого кода можно видеть, что эти перегружаемые версии конструкторов предоставляют значения по умолчанию FileAcces.ReadWrite и FileShare.Read для третьего и четвертого параметров. Также можно создать файловый поток из экземпляра FileInfo:

FileInfo MyFile4 = new FileInfo(@"C:\C# Projects\Projects4.doc");

FileStream fs4 = MyFile4.OpenRead();

FileInfo MyFile5 = new FileInfo(@"C:\C# Projects\Projects5.doc");

FileStream fs5 = MyFile5.OpenWrite();

FileInfo MyFile6 = new FileInfo(@"C:\C# Projects\Projects6.doc");

FileStream fs6 = MyFile6.Open(FileMode.Append, FileAccess.Read, FileShare.None);

FileInfo MyNewFile = new FileInfo(@"C:\C# Projects\ProjectsNew.doc");

FileStream fs7 = MyNewFile.Create();

FileInfo.OpenRead() поставляет поток, предоставляющий доступ только для чтения к существующему файлу, в то время как FileInfo.OpenWrite() предоставляет доступ для чтения-записи. FileInfo.Open() позволяет явно определить параметры режима, доступа и общего доступа.

Не забудьте, что по окончании работы поток необходимо закрыть:

fs.Close();

Закрытие потока освобождает связанные с ним ресурсы и позволяет другим приложениям настроить потоки на тот же самый файл. Для чтения и записи данных в поток FileStream реализует ряд методов.

Метод ReadByte() является простейшим способом чтения данных: он захватывает один байт из потока и преобразовывает результат в int, имеющее значение между 0 и 255. По достижении конца потока возвращается -1:

int NextByte = fs.ReadByte();

Если желательно прочитать несколько байтов за один раз, можно вызвать метод Read(), который читает указанное число байтов в массиве. Read() возвращает реально прочитанное число байтов, если это значение равно нулю, то это говорит о конце потока:

// считать 100 байтов int nBytes = 100;

byte [] ByteArray = new byte[nBytes];

int nBytesRead = fs.Read(ByteArray, 0, nBytes);

Второй параметр в методе Read() — смещение, которое указывает операции Read начать заполнение массива с элемента, отличного от первого.

Если требуется записать данные в файл, то существует два параллельных метода WriteByte() и Write(). WriteByte() записывает один байт в поток:

byte Next Byte = 100; fs.WriteByte(NextByte);

Write(), с другой стороны, записывает массив байтов:

// чтобы записать 100 байтов

int nBytes = 100;

byte [] ByteArray = new byte[nBytes];

// здесь массив заполняется значениями, которые надо записать

fs.Write(BуteArray, 0, nBytes);

Как и для метода Read(), второй параметр позволяет начать записывать с некоторого места, отличного от начала массива. Оба метода WriteByte() и Write() возвращают void.

Помимо этих методов FileStream реализует также другие методы и свойства для выполнения задач учета, типа определения числа байтов в потоке, блокирования потока или очистки буфера. Эти методы обычно не требуются для базового чтения и записи, а если они потребуются, все детали находятся в документации MSDN.

Пример: объект чтения двоичного файла

Для иллюстрации использования класса FileStream напишем пример BinaryFileReader, который считывает и выводит любой файл. Он создается в Visual Studio.NET как оконное приложение. Добавляем один пункт меню, который выводит стандартный диалог OpenFileDialog, запрашивающий файл для чтения, а затем выводит файл. Так как мы читаем двоичные файлы, нам необходимо иметь возможность выводить непечатные символы. Делаем это, выводя каждый байт файла отдельно, по 16 байтов в каждой строке многострочного текстового поля. Если байт представляет печатный символ ASCII, выводится этот символ, иначе выводится значение байта в шестнадцатеричном формате. В любом случае, мы дополняем выводимый текст пробелами, так что каждый выводимый 'байт' занимает четыре столбца, чтобы байты аккуратно выровнялись друг под другом.

Вот как выглядит работа BinaryFileReader при просмотре текстового файла (так как BinaryFileReader может просматривать любой файл, вполне возможно использовать его для текстовых файлов так же, как и для двоичных.) В этом случае пример считывает файл, содержащий переговоры, которые происходили в известной игре "Цивилизация".

Очевидно, что этот формат больше подходит для просмотра значений отдельных байтов, а не для вывода текста. Позже в этой главе будет дан пример, специально созданный для чтения текстовых файлов, тогда можно будет увидеть содержимое такого файла. С другой стороны, преимущество этого примера состоит в том, что мы можем увидеть содержимое любого файла.

Для этого примера запись в файлы не показана. Это связано с тем, что мы не хотим увязнуть в сложностях попыток перевода содержимого текстового поля, представленного выше, в двоичный поток. Мы рассмотрим запись в файлы позже, когда разработаем пример, который может считывать и записывать текстовые файлы.

Итак, запишем код. Вначале добавим дополнительные инструкции using, так как помимо System.IO этот пример будет использовать класс StringBuilder из пространства имен System.Text для создания строк текстового поля:

using System.IO;

using System.Text;

Затем добавляются поля в класс основной формы, одно для представления файлового диалога, и строка, которая задает путь доступа к файлу, просматриваемому в текущий момент:

public class Form1 : System.Windows.Forms.Form {

OpenFileDialog ChooseOpenFileDialog = new OpenFileDialog();

string ChosenFile;

Нам нужно также добавить стандартный код формы Windows для работы методов обработки меню и файлового диалога:

public Form1() {

InitializeComponent();

menuFileOpen.Click += new EventHandler(OnFileOpen);

ChooseOpenFileDialog.FileOk += new CancelEventHandler(OnOpenFileDialogOK);

}

void OnFileOpen(object Sender, EventArgs e) {

ChooseOpenFileDialog.ShowDialog();

}

void OnOpenFileDialogOK(object Sender, CancelEventArgs e) {

ChosenFile = ChooseOpenFileDialog.FileName;

this.Text = ChosenFile;

DisplayFile();

}

Из этого кода мы видим, что, когда пользователь нажимает OK, чтобы выбрать файл в файловом диалоге, вызывается метод DisplayFile(), который реально выполняет работу считывания файла:

void DisplayFile() {

int nCols = 16;

FileStream InStream = new FileStream(ChosenFile, FileMode.Open, FileAccess.Read);

long nBytesToRead = InStream.Length; if (nBytesToRead > 65536/4)

nBytesToRead = 65536/4;

int nLines = (int)(nBytesToRead/nCols) + 1;

String [] Lines = new string[nLines];

int nBytesRead = 0;

for (int i=0; i<nLines; i++) {

StringBuilder NextLine = new StringBuilder();

NextLine.Capacity = 4*nCols;

for (int j = 0; j<nCols; j++) {

int NextByte = InStream.ReadByte();

nBytesRead++;

if (NextByte <0 || nBytesRead > 65536) break;

char NextChar = (char)NextByte;

if (NextChar < 16)

NextLine.Append(" x0" + string.Format("{0,1:X}", (int(NextChar));

else if (char.IsLetterOrDigit(NextChar) || char.IsPunctuation(NextChar))

(NextLine.Append(" " + NextChar + " ");

else NextLine.Append(" x" + string.Format("{0,2:X}", (int)NextChar));

}

Lines[i] = NextLine.ToString();

}

InStream.Close();

this.textBoxContents.Lines = Lines;

}

Разберем данный метод подробнее. Мы создаем экземпляр FileStream для выбранного файла и хотим открыть существующий файл для чтения. Затем мы определяем, сколько существует байтов для чтения и сколько строк должно выводиться. Число байтов обычно равно числу байтов в файле. Однако текстовые поля могут выводить максимум только 65536 символов, и для выбранного формата выводится 4 символа для каждого байта в файле, поэтому необходимо сделать ограничение числа показываемых байтов, если файл длиннее 65536/4 = 16384.

В случае выведения более длинных файлов в таком рабочем окружении можно рассмотреть класс RichTextBox в пространстве имен System.Windows.Forms. RichTextBox аналогичен текстовому полю, но имеет значительно более развитые средства форматирования и не имеет ограничения на объем выводимого текста. Мы используем здесь TextBox, чтобы сохранить тривиальность примера и сосредоточиться на процессе чтения файла.

Основная часть метода представлена в двух вложенных циклах for, которые создают каждую строку текста для вывода. Мы используем класс StringBuilder для создания каждой строки по соображениям производительности. Мы будем добавлять подходящий текст для каждого байта к строке, которая представляет каждую линию 16 раз. Если каждый раз мы будем выделять новую строку и делать копию полусозданной линии, мы не только затратим много времени на выделение строк, но истратим много памяти из кучи динамической памяти. Отметим, что наше определение 'печатных' символов соответствует буквам, цифрам и символам пунктуации, как указано соответствующими статическими методами System.Char. Мы исключили, однако, все символы со значением меньше 16 из списка печатных символов, а значит, будем перехватывать возврат каретки (13) и перевод строки (10) как двоичные символы (многострочное текстовое поле не может правильно вывести эти символы, если они встречаются отдельно внутри строки).

В конце мы закрываем поток и задаем содержимое текстового поля как массив строк, который был создан.

Чтение и запись текстовых файлов

Теоретически вполне возможно использование класса FileStream для чтения и вывода текстовых файлов. В конце концов, мы уже продемонстрировали, как это делается. Формат, в котором выводился выше файл CivNegotiations.txt, был не очень удобен для пользователя, но это было связано не с какой-либо внутренней проблемой класса FileStream, а со способом, который был выбран для вывода результатов в текстовом поле.

Если известно, что определенный файл содержит текст, то будет более удобно прочитать его с помощью классов StreamReader и StreamWriter. Это связано с тем, что эти классы работают на чуть более высоком уровне, и предназначены специально для чтения текста. Методы, которые они реализуют, способны автоматически определять, где находятся удобные точки для остановки чтения, основываясь на содержимом потока. В частности:

□ Эти классы реализуют методы для чтения или записи одной строки текста за раз (StreamReader.ReadLine() и StreamWriter.WriteLine()). В случае чтения, это значит, что поток будет автоматически определять, где находится следующий перевод каретки, и остановит чтение в этой точке. В случае записи это означает, что поток будет автоматически добавлять комбинацию возврата каретки-перевода строки в записываемый текст.

□ При использовании классов StreamReader и StreamWriter не нужно беспокоиться об использованном в файле кодировании. Кодирование означает формат, который имел текст в содержащем его файле. Возможное кодирование включает ASCII (1 байт для каждого символа), или любой из форматов на основе Unicode, UTF7 и UTF8. Текстовые файлы в системах Windows 9х всегда являются ASCII, так как Windows 9х не поддерживает Unicode, и поэтому текстовые файлы могут теоретически содержать данные Unicode, UTF7 или UTF8 вместо данных ASCII. Соглашение состоит в том, что если файл имеет формат ASCII, значит он содержит текст. Если же любой из форматов Unicode, то он будет указан первыми двумя или тремя байтами файла, которые содержат определенную комбинацию значений для указания формата. Эти байты называются маркерами кода байтов. Когда файл открывается с помощью любого стандартного оконного приложения, такого как Notepad или WordPad, то не нужно беспокоиться об этом, так как эти приложения знакомы с различными методами кодирования и автоматически правильно считывают файл. Это также случай для класса StreamReader, который будет правильно считывать файл в любом из этих форматов, в то время как класс StreamWriter способен форматировать текст, который он записывает, с помощью любой запрошенной техники кодирования. С другой стороны, если требуется прочитать и вывести текстовый файл с помощью класса FileStream, придется все это выполнять самостоятельно.

Класс StreamReader

Класс StreamReader используется для чтения текстовых файлов. Создать StreamReader проще, чем экземпляр FileStream, так как некоторые из параметров FileStream не требуются. В частности, режим и тип доступа несущественны, поскольку с помощью StreamReader можно только читать. Также не существует непосредственного параметра для определения полномочий общего доступа. Но с другой стороны, существуют и новые параметры:

□ Необходимо определить, что делать с различными методами кодирования. Можно дать инструкцию StreamReader проверять маркеры кода байтов в файле, чтобы выяснить метод кодирования, или можно просто приказать StreamReader предполагать, что файл использует определенный метод кодирования.

□ Вместо предоставления имени файла для чтения, можно предоставить ссылку на другой поток.

Эта последняя возможность заслуживает дополнительного обсуждения, так как иллюстрирует еще одно преимущество обоснования модели чтения и записи данных вокруг концепции потоков. Поскольку StreamReader работает на относительно высоком уровне, может оказаться полезным в ситуации, когда имеется другой поток, который читает данные из некоторого другого источника, использовать средства, предоставляемые StreamReader для обработки этого потока, как если бы он содержал текст. Это можно сделать, передавая вывод из этого потока в StreamReader. Таким образом, StreamReader может использоваться для чтения и обработки данных из любого источника данных, а не только файлов. Это по сути ситуация, которая рассматривалась ранее в отношении класса BinaryReader. Однако в этой книге мы будем использовать StreamReader только для прямого соединения с файлами.

В силу указанных возможностей StreamReader имеет большое число конструкторов. Помимо этого существует пара методов FileInfo, которые также возвращают ссылки на StreamReader: OpenText() и CreateText(). Здесь мы проиллюстрируем некоторые из конструкторов.

Простейший конструктор получает только имя файла. Этот StreamReader будет проверять маркеры кода байтов, чтобы определить кодирование:

StreamReader sr = new StreamReader(@"C:\My Documents\ReadMe.txt");

И наоборот, если желательно определить, предполагается ли кодирование UTF8:

StreamReader sr = new StreamReader(@"C:\My Documents\ReadMe.txt", Encoding.UTF8Encoding);

Мы определяем кодирование, используя одно из нескольких свойств класса, System.Text.Encoding. Этот класс является абстрактным базовым классом, из которого определяется ряд классов, которые реализуют методы, реально выполняющие кодирование текста. Каждое свойство возвращает экземпляр соответствующего класса. Здесь можно использовать следующие свойства:

□ ASCII

□ Unicode

□ UTF7

□ UTF8

□ BigEndianUnicode

Следующий пример показывает сцепление StreamReader с FileStream. Преимущество этого состоит в том, что можно явно определить, создавать ли файл и полномочия совместного доступа, что невозможно сделать, если напрямую соединять StreamReader с файлом:

FileStream fs =

new FileStream(@"C:\My Documents\ReadMe.txt", FileMode.Open,

FileAccess.Read, FileShare.None);

StreamReader sr = new StreamReader(fs);

Для этого примера мы уточняем., что StreamReader будет искать маркеры кода байтов, чтобы определись используемый метод кодирования, так он будет делать и в следующих примерах, где StreamReader получают из экземпляра FileInfo:

FileInfo MyFile = new FileInfo(@"C:\My Documents\ReadMe.txt");

StreamReader sr = MyFile.OpenText();

Также как с FileStream, необходимо всегда закрывать StreamReader после использования. Невыполнение этого приведет к файлу, остающемуся заблокированным для других процессов (если только для создания StreamReader не использовался FileStream со спецификацией FileShare.ShareReadWrite).

sr.Close();

Теперь мы перешли к проблеме создания экземпляра StreamReader. Так же, как с классом FileStream, мы просто указываем различные способы чтения данных и оставляем другие, менее часто используемые, методы StreamReader для документации MSDN.

Возможно, простейшим в использовании является метод ReadLine(), который продолжает чтение, пока не доходит до конца строки. Он не включает комбинацию возврат каретки-перевода строки, которая отмечает конец строки в возвращаемой строке:

string NextLine = sr.ReadLine();

Альтернатива — захватить весь остаток файла (или строго говоря, остаток потока) в одной строке:

string RestOfStream = sr.ReadToEnd();

Можно также прочитать один символ:

int NextChar = sr.Read();

Эта конструкция с Read() преобразует возвращаемый символ в int. Это делается так, потому что имеется возможность альтернативного возврата -1, если будет достигнут конец потока.

Наконец, можно прочитать заданное число символов в массив с использованием смещения:

// прочитать 100 символов

int nChars = 100;

chr [] CharArray = new char[nChars];

int nCharsRead = sr.Read(CharArray, 0, nChars);

nCharsRead будет меньше nChars, если запрос чтения потребует больше символов, чем осталось в файле.

Класс StreamWriter

Он работает практически таким же образом, как и StreamReader, за исключением того только, что StreamWriter используется для записи в файл (или в другой поток). Возможности создания StreamWriter включают в себя:

StreamWriter sw = new StreamWriter(@"C:\My Documents\ReadMe.txt");

Приведенный выше код будет использовать кодирование UTF8, которое рассматривается в .NET как метод кодирования по умолчанию. Если желательно определить альтернативное кодирование:

StreamWriter sw = new StreamWriter(@"C:\My Docurnents\ReadMe.txt", true, Encoding.ASCII);

В этом конструкторе вторым параметром является Boolean, который указывает, должен ли файл быть открыт для добавления. Странно, но не существует конструктора, который получает только имя файла и класс кодирования.

Конечно, можно соединить StreamWriter с файловым потоком, чтобы предоставить больший контроль над параметрами открытия файла:

FileStream fs = new FileStream(@"C:\Му Documents\ReadMe.txt",

FileMode.CreateNew, FileAcces.Write, FileShare.ShareRead);

StreamWriter sw = new StreamWriter(fs);

FileInfo не реализует никаких методов, которые возвращают StreamWriter. Альтернативно, если вы захотите создать новый файл и начать записывать в него данные, то может оказаться полезной такая последовательность действий:

FileInfo MyFile = new FileInfo(@"C:\My Documents\NewFile.txt");

StreamWriter sw = MyFile.CreateText();

Так же, как и со всеми другими потоковыми классами, важно закрыть StreamWriter, когда работа закончена:

sw.Close();

Запись в поток делается с помощью любого из четырех перегружаемых методов StreamWriter.Write(). Простейший из них записывает строку и дополняет ее комбинацией возврат каретки — перевод строки:

string NextLine = "Groovy Line";

sw.Write(NextLine);

Можно также записать один символ:

char NextChar = 'а';

sw.Write(NextChar);

Массив символов также возможен:

char [] Char Array = new char[100];

// инициализация этого массива символов

sw.Write(CharArray);

Можно даже записать часть массива символов:

int nCharsToWrite = 50;

int StartAtLocation = 25;

char [] CharArray = new char[100];

// инициализируем эти символы

sw.Write(CharArray, StartAtLocation, nCharsToWrite);

Пример: ReadWriteText

Пример ReadWriteText выводит результат использования классов StreamReader и StreamWriter. Он похож на предыдущий пример ReadBinaryFile, но предполагает, что считываемый файл является текстовым файлом, и выводит его в этом виде. Он может сохранить файл (со всеми изменениями, которые будут сделаны в тексте в текстовом поле). Он будет сохранять любой файл в формате Unicode.

Снимок показывает использование ReadWriteText для вывода того же файла CivNegotiations, который использовался раньше. В этот раз, однако, мы сможем прочитать содержимое гораздо легче.

Не будем вдаваться в детали добавления методов обработки событий для диалога открытия файла, поскольку они по сути такие же, что и в примере BinaryFileReader. Как и для данного примера, открытие нового файла приводит к вызову метода DisplayFile(). Единственное реальное различие между этим примером и предыдущим состоит в реализации DisplayFile, а также в том факте, что теперь имеется возможность сохранить файл. Она представлена другим пунктом меню — save. Обработчик для этого пункта вызывает другой добавленный в код метод — SaveFile(). (Отметим, что этот файл всегда перезаписывает первоначальный файл, этот пример не имеет возможности записи в другой файл.)

Посмотрим сначала на SaveFile, так как это простейшая функция. Мы просто записываем каждую строку текстового поля по очереди в поток StreamWriter, полагаясь на метод StreamReader.WriteLine() для добавления комбинации возврата каретки-перевода строки в конце каждой строки:

void SaveFile() {

StreamWriter sw = new StreamWriter(ChosenFile, false, Encoding.Unicode);

foreach (string Line in textBoxContents.Lines) sw.WriteLine(Line);

sw.Close();

}

ChosenFile является строковым полем основной формы, которое содержит имя прочитанного файла (как и в предыдущем примере). Отметим, что при открытии потока определяется кодирование Unicode. Если желательно записывать файлы в другом формате, необходимо изменить значение этого параметра. Второй параметр в этом конструкторе будет задан как true, если мы хотим добавлять к файлу, но в данном случае мы этого не делаем. Кодирование должно задаваться во время создания объекта записи потока. В дальнейшем оно доступно только для чтения как свойство Encoding.

Теперь проверим, как считываются файлы. Процесс чтения осложняется тем, что мы не знаем, пока не прочитаем файл, сколько строк он будет содержать (другими словами, сколько последовательностей (char)13 - (char)10 находится в файле). Решим эту проблему, считывая сначала файл в экземпляр класса StringCollection, который находится в пространстве имен System.Collections.Specialized. Этот класс создан для хранения множества строк, которые могут динамически увеличиваться. Он реализует два интересующих нас метода: Add(), добавляющий строку в коллекцию, и CopyTo(), который копирует коллекцию строк в обычный массив (экземпляр System.Array). Каждый элемент объекта StringCollection будет содержать одну строку файла.

Метод DisplayFile() вызывает другой метод ReadFileIntoStringCollection(), который реально считывает файл. После этого мы знаем, сколько имеется строк, поэтому можно скопировать StringCollection в обычный массив фиксированного размера и передать этот массив в текстовое поле. Так как при создании копии копируются только ссылки на строки, а не сами строки, процесс будет достаточно эффективным:

void DisplayFile() {

StringCollection LinesCollection = ReadFileIntoStringCollection();

string [] LinesArray = new string[LinesCollection.Count];

LinesCollection.CopyTo(LinesArray, 0);

this.textBoxContents.Lines = LinesArray;

}

Второй параметр StringCollection.CopyTo() указывает индекс в массиве назначения, где мы хотим начать размещение коллекции.

Теперь рассмотрим метод ReadFileIntoStringCollection(). Мы используем StreamReader для считывания каждой строки. Основной трудностью является необходимость подсчитывать считанные символы, чтобы не превысить емкость текстового поля:

ArrayList ReadFileIntoStringCollection() {

const int MaxBytes = 65536;

StreamReader sr = new StreamReader(ChosenFile);

StringCollection Result = new StringCollection();

int nBytesRead = 0;

string NextLine;

while ((NextLine = sr.ReadLine()) != null) {

nBytesRead += NextLine.Length;

if (nBytesRead > MaxBytes) break;

Result.Add(NextLine);

}

sr.Close();

return Result;

}

Код завершен.

Если выполнить ReadWriteText — считать файл CivNegotiations и затем сохранить его, то файл будет иметь формат Unicode. Это невозможно для любого из обычных оконных приложений: Notepad, Wordpad и даже наш собственный пример ReadWriteText будут по-прежнему считывать файл и выводить его правильно в Windows NT/2000/XP, хотя, так как Windows 9х не поддерживает Unicode, приложения типа Notepad не смогут понять файл Unicode на этих платформах (Если загрузить пример с web-сайта издательства Wrox Press, то можно попробовать это сделать.) Однако, если попробовать вывести файл снова с помощью предыдущего примера ReadBinaryFile, то разница будет заметна немедленно, как на следующем экране. Два начальных файла указывают, что файл имеет формат Unicode, и поэтому мы видим, что каждый символ представлен двумя байтами. Этот последний факт вполне очевиден, поскольку старший байт каждого символа в данном конкретном файле равен нулю, поэтому каждый второй байт в этом файле выводится теперь как x00:

Чтение и запись в реестр

Во всех версиях Windows, начиная с Windows 95, реестр был центральным репозиторием всех конфигурационных данных, связанных с настройкой Windows, предпочтениями пользователя и установленным программным обеспечением и устройствами. Почти все коммерческое программное обеспечение сегодня использует реестр для хранения информации о себе, и компоненты COM должны помещать информацию о себе в реестр, чтобы клиент смог их вызвать. Платформа .NET и сопровождающая ее концепция о нулевом влиянии установки слегка уменьшили значение реестра для приложений в том смысле, что сборки являются полностью самодостаточными, поэтому никакая информация об определенных сборках не должна помещаться в реестр для сборок общего использования.

Тот факт, что приложения могут теперь устанавливаться с помощью программы установки Windows также освобождает разработчика от некоторых прямых манипуляций с реестром, которые обычно были связаны с установкой приложений. Однако, несмотря на это, при распространении любого законченного приложения вполне вероятно, что это приложение будет использовать реестр для хранения информации о своей конфигурации. Если приложение появляется в диалоговом окне Add/Remove Programs в панели управления, то создается соответствующая запись в реестре.

Как и можно было ожидать от такой всеобъемлющей библиотеки, как библиотека .NET, она содержит классы, которые предоставляют доступ к реестру. Имеются два класса, связанных с реестром, и оба находятся в пространстве имен Microsoft.Win32. Это классы Registry и RegistryKey. Прежде чем рассматривать их, кратко разберем структуру самого реестра.

Реестр

Реестр имеет иерархическую структуру, очень похожую на файловую систему. Обычно просмотр или изменение содержимого реестра выполняется с помощью одной из двух утилит — regedit или regedt32. regedit стандартно поставляется со всеми версиями Windows, начиная с Windows 95. Утилита regedt32 поставляется с Windows NT и Windows 2000 — она менее дружественна пользователю, чем regedit, но разрешает доступ к данным безопасности, которые regedit не может видеть. Здесь будет использоваться regedit, которую можно запустить, вводя regedit в диалоговом окне Run… или командной строке.

При первом запуске regedit появится примерно следующее изображение:

Regedit имеет интерфейс пользователя в стиле представлений дерева/списка, аналогичный Проводнику Windows, который соответствует иерархической структуре самого реестра. Однако, как мы скоро увидим, существуют некоторые различия.

В файловой системе узлами самого верхнего уровня можно считать разделы диска C:\, D:\ и т.д. В реестре эквивалентом разделу является улей реестра. Невозможно изменить существующие ульи, они являются фиксированными, и всего существует семь ульев (хотя с помощью regedit можно увидеть только пять):

□ HKEY_CLASSES_ROOT (HKCR) содержит данные о типах файлов в системе (.txt, .doc и т.д.) и приложениях, которые могут открывать файлы каждого типа. Включает также информацию о регистрации для всех компонентов COM (эта область является обычно наибольшей областью реестра, так как Windows сегодня поставляется с огромным числом компонентов COM).

□ HKEY_CURRENT_USER (HKCU) содержит данные о предпочтениях пользователя, который в данный момент зарегистрирован на машине.

□ HKEY_LOCAL_MACHINE (HKLM) является огромным ульем, который содержит данные обо всем программном обеспечении и оборудовании, установленном на машине. Он также содержит улей HKCR: HKCR в действительности не является независимым ульем со своими собственными правами, а является просто удобным отображением на ключ реестра HKLM/SOFTWARE/Classes.

□ HKEY_USERS (HKUSR) содержит данные о пользовательских предпочтениях всех пользователей. Как можно догадаться, он содержит также улей HKCU, который является отображением на один из ключей в HKEY_USERS).

□ HKEY_CURRENT_CONFIG (HKCF) содержит данные об оборудовании компьютера.

Оставшиеся два ключа содержат информацию, которая имеет временный характер и которая часто изменяется:

□ HKEY_DYN_DATA является общим контейнером для любых изменчивых данных, которые необходимо хранить где-то в реестре.

□ HKEY_PERFORMANCE_DATA содержит данные, связанные с производительностью выполняющихся приложений.

Внутри ульев находится древовидная структура ключей реестра. Каждый ключ является во многом аналогом папки или файла в файловой системе. Однако существует одно очень важное различие: файловая система различает файлы (которые предназначены для хранения данных) и папки (которые предназначены прежде всего для хранения других файлов или папок), но в реестре существуют только ключи. Ключ может содержать как данные, так и другие ключи.

Если ключ содержит данные, то они будут представлены как последовательность значений (можно привести доводы, что эти значения и есть грубые эквиваленты файлов в файловой системе). Каждое значение будет иметь имя, тип данных и значение. Кроме того, ключ может иметь значение по умолчанию, не имеющее имени.

Эту структуру можно увидеть, используя regedit для проверки ключей реестра. На экране показано содержимое ключа HKCU/Control Panel/Appearance, в котором находятся данные выбранной цветовой схемы зарегистрированного в данный момент пользователя. regedit показывает, какой ключ проверяется, выводя его с изображением открытой папки в древовидном представлении:

Ключ HKCU/Control Panel/Appearance имеет три заданных именованных значения, хотя значение по умолчанию не содержит никаких данных. Столбец на экране, помеченный Type, указывает тип данных каждого значения. Записи в реестре можно форматировать как один из трех типов данных. Этими типами являются REG_SZ (что грубо соответствует экземпляру строки .NET — соответствие неточное, так как типы данных реестра не являются типами данных .NET), REG_DWORD (грубо соответствует uint), и REG_BINARY (массив байтов).

Приложение, которое хочет сохранить данные в реестре, будет делать это создавая ряд ключей реестра, обычно с ключом HKLM/Software/<ИмяКомпании>. Отметим, что эти ключи не обязательно должны содержать какие-либо данные. Иногда сам факт существования ключа предоставляет достаточно информации для приложения.

Классы реестра в .NET

Доступ к реестру осуществляется с помощью двух классов в пространстве имен Microsoft.Win32 — Registry и RegistryKey. Экземпляр RegistryKey представляет ключ реестра. Этот класс реализует методы для доступа к ключам-потомкам, для создания новых ключей или для чтения или изменения значений ключа. Другими словами, чтобы делать все необходимое с ключом реестра (за исключением задания уровней безопасности для ключа). RegistryKey является классом, который будет использоваться практически для любой работы с реестром. Registry, напротив, является классом, экземпляры которого никогда не создаются. Его роль состоит в предоставлении экземпляров RegistryKey, которые являются ключами верхнего уровня, различными ульями, чтобы начать перемещение по реестру. Registry предоставляет эти экземпляры через семь статических свойств, называемых соответственно ClassesRoot, CurrentConfig, CurrentUser, DynData, LocalMachine, PerformanceData и Users.

Поэтому, например, чтобы получить экземпляр RegistryKey, который представляет ключ HKLM, необходимо написать:

RegistryKey Hklm = Registry.LocalMachine;

Процесс получения ссылки на объект RegistryKey называют открытием ключа.

Можно было бы ожидать, что методы объекта RegistryKey будут подобны методам класса DirectoryInfo при условии, что реестр имеет иерархическую структуру, аналогичную файловой системе, но в действительности это не так. Часто способ доступа к реестру отличается от способа, который используется для файлов и папок, и RegistryKeyреализует методы, которые это отражают.

Наиболее очевидное различие состоит в том, как открывают ключ реестра в заданном месте в реестре. Класс Registry не имеет никаких открытых конструкторов, которые можно использовать, он не имеет также никаких методов, которые позволят перейти прямо к ключу, задавая его имя. Вместо этого ожидается, что вы спуститесь вниз к этому ключу с вершины соответствующего улья. Если желательно создать экземпляр объекта RegistryKey, то единственный способ — начать с соответствующего статического свойства Registry и двигаться оттуда вниз. Поэтому, например, если нужно прочитать некоторые данные в ключе HKLM/Software/Microsoft, то ссылку на него можно получить следующим образом:

RegistryKey Hklm = Registry.LocalMachine;

RegistryKey HkSoftware = Hklm.OpenSubKey("Software");

RegistryKey HkMicrosoft = HkSoftware.OpenSubKey("Microsoft");

Доступ к ключу реестра, полученный таким образом, будет предоставлен только для чтения. Если вы хотите иметь возможность записи в ключ (что предполагает запись в его значения либо создание или удаление его прямых потомков), необходимо использовать другую перегруженную версию OpenSubKey, которая получает второй параметр типа bool, указывающий, требуется ли иметь доступ к ключу для чтения-записи. Поэтому, например, при желании модифицировать ключ Microsoft (предполагая, что вы являетесь системным администратором с полномочиями на это), необходимо написать следующее:

RegistryKey Hklm = Registry.LocalMachine;

RegistryKey HkSoftware = Hklm.OpenSubKey("Software");

RegistryKey HkMicrosoft = HkSoftware.OpenSubKey("Microsoft", true);

В связи с этим, так как этот ключ содержит информацию, используемую приложениями Microsoft, в большинстве случаев нежелательно модифицировать этот конкретный ключ.

Метод OpenSubKey() будет вызываться, если ожидается, что ключ уже присутствует. Если ключ отсутствует, то он возвращает ссылку null. Если желательно создать ключ, то необходимо использовать метод CreateSubKey() (который автоматически предоставляет доступ к ключу для чтения-записи через возвращаемую ссылку):

RegistryKey Hklm = Registry.LocalMachine;

RegistryKey HkSoftware = Hklm.OpenSubKey("Software");

RegistryKey HkMine = HkSoftware.CreateSubKey("MyOwnSoftware");

Способ работы CreateSubKey() достаточно интересен: он будет создавать ключ, если тот еще не существует, а если уже существует, то будет возвращать экземпляр RegistryKey, который представляет существующий ключ. Причина такого поведения метода состоит в способе использования реестра. Реестр в целом содержит долговременные данные, например, конфигурационную информацию для Windows и различных приложений. Поэтому не очень часто возникает ситуация, когда необходимо явно создать ключ.

Более распространена ситуация, когда приложению необходимо убедиться, что некоторые данные присутствуют в реестре, другими словами, создать соответствующие ключи, если они еще не существуют, но ничего не делать, если они существуют. Метод CreateSubKey() прекрасно с этим справляется. В отличие, например, от ситуации с FileInfo.Open() у CreateSubKey() нет возможности случайно удалить какие-либо данные. Если целью действительно является удаление ключей реестра, то необходимо явно вызвать метод RegistryKey.Delete(). Это имеет смысл с учетом важности реестра для Windows. Меньше всего хотелось бы полностью разрушить Windows, удалив пару важных ключей во время отладки обращений к реестру в C#.

Когда ключ реестра для чтения или модификации найден, можно использовать методы SetValue() или GetValue() для задания или получения из них данных. Оба эти метода получают в качестве параметра строку, задающую имя значения, a SetValue() требует дополнительно ссылку на объект, содержащий детали значения. Так как параметр определяется как объектная ссылка, то он может действительности быть ссылкой на любой класс по желанию. SetValue() будет определять по типу реально предоставленного класса, как задать значение REG_SZ, REG_DWORD или REG_BINARY. Например:

RegistryKey HkMine = HkSoftware.CreateSubKey("MyOwnSoftware");

HkMine.SetValue("MyStringValue", "Hello World");

HkMine.SetValue(MyIntValue", 20);

Этот код задает для ключа два значения: MyStringValue будет иметь тип REG_SZ, а MyIntValue — тип REG_DWORD. В последующем примере будут рассмотрены только эти два типа.

RegistryKey.GetValue() работает по большей части таким же образом. Он определен для возврата объектной ссылки, а значит, он может на самом деле вернуть ссылку на string, если обнаружит значение типа REG_SZ, и int, если это значение имеет тип REG_DWORD:

string StringValue = (string)HkMine.GetValue("MyStringValue");

int IntValue = (int)HkMine.Get.Value("MyIntValue");

И наконец, по окончании чтения или модификации данных ключ необходимо закрыть:

HkMine.Close();

Помимо этих методов, RegistryKey реализует большое число других методов и свойств. Таблицы представляют выборку наиболее употребительных:

Свойства

Имя свойства

Назначение

Name

Имя ключа (только для чтения)

SubKeyCount

Число потомков этого ключа

ValueCount

Сколько значений содержит ключ

Методы

Имя

Назначение

Close()

Закрывает ключ

CreateSubKey()

Создает подключ с заданным именем (или открывает его, если он уже существует)

DeleteSubKey()

Удаляет заданный подключ

DeleteSubKeyTree()

Рекурсивно удаляет подключ и всех его потомков

DeleteValue()

Удаляет именованное значение из ключа

GetSubKeyNames()

Возвращает массив строк, содержащих имена подключей

GetValue()

Возвращает именованное значение

GetValueNames()

Возвраает массив строк, содержащих имена всех значений ключа

OpenSubKey()

Возвращает ссылку на экземпляр RegistryKey, который представляет заданный подключ

SetValue()

Задает именованное значение

Пример: SelfPlacingWindow

Проиллюстрируем использование классов реестра с помощью приложения, которое называется SelfPlacingWindow. Этот пример является простым приложением Windows на C#, которое в действительности почти не имеет свойств. Единственное, что можно сделать в этом приложении, это щелкнуть по кнопке, что приведет к появлению стандартного диалогового окна выбора цветов в Window (представляемому классом System.Windows.Forms.ColorDialog), чтобы можно было выбрать цвет который станет фоновым цветом формы

Это приложение обладает одним важным свойством, отсутствующим практически у всех остальных приложений разрабатываемых в этой книге. Если перетащить окно по экрану, изменить его размер, развернуть на весь экран или свернуть его перед выходом из приложения, оно будет запоминать новое положение, а также цвет фона, чтобы в следующий раз при запуске автоматически восстанавливалось последнее выбранное состояние. Оно запоминает эту информацию, так как записывает ее в реестр, когда закрывается. Таким образом, будут продемонстрированы не только сами классы реестра из .NET, но также типичное их использование, которое наверняка захочется скопировать в любое серьезное разрабатываемое коммерческое приложение Window.

Местом, где SelfPlacingWindow хранил свою информацию в реестре, является ключ HKLM/Software/WroxPress/SelfPlacingWindow. HKLM является обычным местом для хранения информации о конфигурации приложений, но отметим, что оно не является специфическим для пользователя. Вероятно, вам понадобится скопировать эту информацию в улей HK_Users, чтобы каждый пользователь имел свой собственный профиль.

При первом запуске этого примера он будет искать этот ключ и, очевидно, не найдет его. Поэтому вынужден будет использовать значения по умолчании для размера, цвета и позиции, которые задаются в среде разработчика. Пример имеет также окно списка, где выводит всю информацию, прочитанную в реестре. При первом запуске оно будет выглядеть следующим образом:

Если теперь изменить цвет фона и переместить или изменить размер окна приложения SelfPlacingWindow, оно создаст перед завершением ключ HKLM/Software/WroxPress/SelfPlacingWindow и запишет в него свою новую конфигурационную информацию. Можно проверить эту информацию с помощью regedit:

На этом экране можно видеть, что SelfPlacingWindow помещает ряд значений в ключ реестра.

Значения Red, Green и Blue задают компоненты цветов, которые формируют выбранный цвет фона. О компонентах цвета подробно рассказывается в главе 21. Любой изображаемый цвет в системе может быть полностью описан этими тремя компонентами, каждый из которых представляется числом между 0 и 255 (или 0x00 и 0xff в шестнадцатеричном представлении). Указанные здесь значения задают ярко-зеленый цвет. Существуют также четыре дополнительных значения REG_DWORD, которые представляют положение и размер окна: X и Y являются координатами верхнего левого угла окна на рабочем столе. Width и Height задают размер окна WindowsState является единственным значением, для которого мы использовали строковый тип данных (REG_SZ), и оно может содержать одну из строк normal, maximized или minimized, в зависимости от конечного состояния окна при выходе из приложения.

Если теперь запустить SelfPlacingWindow снова, то оно будет считывать этот ключ реестра и автоматически позиционировать себя соответственно:

В этот раз, когда происходит выход из SelfPlacingWindow, приложение будет перезаписывать предыдущие настройки в реестре новыми значениями, существующими во время выхода из приложения. Чтобы создать код примера, мы создаем проект Windows Forms в Visual Studio.NET и добавляем окно списка и кнопку, используя набор инструментов среды разработчика. Мы изменим имена этих элементов управления соответственно на listBoxMessages и buttonChooseColor. Также необходимо обеспечить использование пространства имен Microsoft.Win32.

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

using Microsoft.Win32;

Нам необходимо добавить одно поле к основному классу Form1, который будет представлять окно диалога выбора цвета.

public class Form1 : System.Windows.Forms.Form {

/// <summary>

/// Обязательная переменная проектировщика.

/// </summary>

private System.СomponentModel.Container components;

private System.Windows.Forms.ListBox ListBoxMessages;

private system.Windows.Forms.Button buttonChooseColor;

ColorDialog ChooseColorDialog = new ColorDialog();

Довольно много действий происходит в конструкторе Form1:

public Form1() {

InitializeComponent();

buttonChooseColor.Click += new EventHandler(OnClickChooseColor);

try {

if (ReadSettings() == false)

listBoxMessages.Items.Add("No information in registry");

else

listBoxMessages.Items.Add("Information read in from registry");

StartPosition = FormStartPosition.Manual;

} catch (Exception e) {

listBoxMessages.Items.Add("A problem occured reading in data from registry:");

listBoxMessages.Items.Add(e.Message);

}

}

В этом конструкторе мы начинаем с создания метода обработки события нажатия пользователем кнопки. Обработчиком является метод с именем OnClickChooseColor (см. ниже). Считывание конфигурационной информации делается с помощью другого метода — ReadSettings(). ReadSettings() возвращает true, если находит информацию в реестре, и false, если не находит (что будет, по-видимому, иметь место, так как приложение выполняется первый раз). Мы помещаем эту часть конструктора в блок try на случай возникновения каких-либо исключений при считывании значений реестра (это может произойти, если вмешался некоторый пользователь и сделал какие-то изменения с помощью regedit).

Инструкция StartPosition = FormStartPosition.Manual; говорит форме взять свою начальную позицию из свойства DeskTopLocation вместо используемого по умолчанию положения в Window (поведение по умолчанию). Возможные значения берутся из перечисления FormStartPosition.

SelfPlacingWindow также является одним из немногих приложений в этой книге, для которого существенно используется добавление кода в метод Dispose(). Напомним, что Dispose() вызывается, когда приложение завершается нормально, так что это идеальное место для сохранения конфигурационной информации в реестре. Это делается с помощью другого метода, который будет написан,— SaveSettings():

/// <summary>

/// Очистить все использованные ресурсы

/// </summary>

public override void Dispose() {

SaveSettings();

base.Dispose();

if(components != null) components.Dispose();

}

SaveSettings() и ReadSettings() являются методами, которые содержат код для работы с интересующим нас реестром, но прежде чем их рассматривать, необходимо разобраться с обработкой события, возникающего при нажатии пользователем на кнопку. Это предполагает вывод диалогового окна выбора цвета и задание цвета фона в соответствии с выбором пользователя:

void OnClickChooseColor(object Sender, EventArgs e) {

if (ChooseColorDialog.ShowDialog() == DialogResult.OK)

BackColor = ChooseColorDialog.Color;

}

Теперь посмотрим, как сохраняются настройки:

void SaveSettings() {

RegistryKey SoftwareKey = Registry.LocalMachine.OpenSubKey("Software", true);

RegistryKey WroxKey = SoftwareKey.CreateSubKey("WroxPress");

RegistryKey SelfPlacingWindowKey = WroxKey.CreateSubKey("SelfPlacingWindowKey");

SelfPlacingWindowKey.SetValue("BackColor", (object)BackColor.ToKnownColor());

SelfPlacingWindowKey.SetValue("Red", (object)(int) BackColor.R);

SelfPlacingWindowKey.SetValue("Green", (object)(int)BackColor.G);

SelfPlacingWindowKey.SetValue("Blue", (object)(int)Backcolor.В);

SelfPlacingWindowKey.SetValue("Width", (object)Width);

SelfPlacingWindowKey.SetValue("Height", (object)Height);

SelfPlacingWindowKey.SetValue("X", (object)DesktopLocation.X);

SelfPlacingWindowKey.SetValue("Y", (object)DesktopLocation.Y);

SelfPlacingWindowKey.SetValue("WindowState", (object)WindowState.ToString());

}

Мы начали с перемещения в реестре, чтобы получить ключ реестра HKLM/Software/WroxPress/SelfPlacingWindow с помощью продемонстрированной выше техники, начиная со статического свойства Registry.LocalMachine, которое представляет улей HKLM:

RegistryKey SoftwareKey = Registry.LocalMachine.OpenSubKey("Software" , true);

RegistryKey WroxKey = SoftwareKey.CreateSubKey("WroxPress");

RegistryKey SelfPlacingWindowKey = WroxKey.CreateSubKey("SelfPlacingWindowKey");

Мы используем метод RegistryKey.OpenSubKey(), а не RegistryKey.CreateSubKey(), позволявший добраться до ключа HKLM/Software. Так происходит вследствие уверенности, что этот ключ уже существует, в противном случае имеется серьезная проблема с компьютером, так как этот ключ содержит настройки для большого объема системного программного обеспечения. Мы также указываем, что нам требуется доступ для записи в этот ключ. Это вызвано тем, что если ключ WroxPress еще не существует, нам нужно будет его создать, что включает запись в родительский ключ.

Следующий ключ для перехода — HKLM/Software/WroxPress, но так как мы не уверены, что ключ уже существует, то используем CreateSubKey() для его автоматического создания, если он не существует. Отметим, что CreateSubKey() автоматически предоставляет доступ для записи к рассматриваемому ключу. Когда мы достигнем HKLM/Software/Wrox. Press/SelfPlacingWindow, то останется просто вызвать метод RegistryKey.SetValue() несколько раз, чтобы создать или задать соответствующие значения. Существуют, однако, некоторые осложнения.

Первое. Можно заметить что мы задействуем пару классов, которые раньше не встречались: свойство DeskTopPosition класса Form указывает позицию верхнего левого угла экрана и имеет тип Point. Рассмотрим структуру Point в главе GDI+. Здесь необходимо знать только, что она содержит два целых числа — X и Y, которые представляют горизонтальную и вертикальную позиции на экране. Мы также используем три свойства члена класса Color: R, G и B. Color представляет цвет, а его свойства задают красный, зеленый и синий компоненты, которые составляют цвет и имеют тип byte. Также применяется свойство Form. WindowState, содержащее перечисление, которое задает текущее состояние окна — minimized, maximized или restored.

Отметим, что при преобразовании типов SetValue() получает два параметра: строку, которая задает имя ключа, и экземпляр System.Object, содержащий значение. SetValue имеет возможность выбора формата для хранения значения, он может сохранить его как REG_SZ, REG_BINARY или REG_DWORD, и он в действительности делает правильный выбор в зависимости от заданного типа данных. Поэтому для WindowsState передается строка и SetValue() определяет, что она должна быть преобразована в REG_SZ. Аналогично для различных позиций и размеров, которые мы передаем, целые значения будут преобразованы в REG_DWORD. Однако компоненты цвета являются более сложными, но мы хотим, чтобы они также хранились как REG_DWORD, потому что они имеют числовые типы. Однако если метод SetValue() видит, что данные имеют тип byte, он будет сохранять их гак строку REG_SZ в реестре. Чтобы избежать этого, преобразуем компоненты цвета в int.

Мы также явно преобразуем все значения в тип object. На самом деле мы не обязаны этого делать, так как преобразование из любого типа данных в тип object выполняется неявно, но мы делаем это, чтобы в действительности показать происходящее и напомнить, что SetValue определен для получения объектной ссылки в качестве второго параметра.

Метод ReadSettings() немного длиннее, так как каждое считанное значение нужно также проинтерпретировать, чтобы вывести значение в окне списка и сделать подходящие изменения в соответствующем свойстве основной формы. ReadSettings() выглядит следующим образом:

bool ReadSettings() {

RegistryKey SoftwareKey = Registry.LocalMachine.OpenSubKey("Software"));

RegistryKey WroxKey = SoftwareKey.OpenSubKey("WroxPress");

if (WroxKey == null) return false;

RegistryKey SelfPlacingWindowKey = WroxKey.OpenSubKey("SelfPlacingWindow");

if (SelfPlacingWindowKey == null) return false;

else

listBoxMessages.Items.Add("Successfully opened key " + SelfPlacingWindowKey.ToString());

int RedComponent = (int)SelfPlacingWindowKey.GetValue("Red");

int GreenComponent = (int)SelfPlacingWindowKey.GetValue("Green");

int BlueComponent = (int)SelfPlacingWindowKey.GetValue("Blue");

BackColor = Color.FromArgb(RedComponent, GreenComponent, BlueComponent);

listBoxMessages.Items.Add("Background color: " + BackColor.Name);

int X = (int(SelfPlacingWindowKey.GetValue("X");

int Y = (int)SelfPlacingWindowKey.GetValue("Y");

DesktopLocation = new Point(X, Y);

listBoxMessages.Items.Add("Desktop location: " + DesktopLocation.ToString());

Height = (int)SelfPlacingWindowKey.GetValue("Height");

Width = (int)SelfPlacingWindowKey.GetValue("Width");

listBoxMessages.Items.Add("Size: " + new Size(Width, Height).ToString());

string InitialWindowState = (string)SelfPlacingWindowKey.GetValue("WindowState");

listBoxMessages.Items.Add("Window State: " + InitialWindowState);

WindowState =

(FormWindowState)FormWindowState.Parse(WindowState.GetType() , InitialWindowState)

return true;

}

В ReadSettings() мы должны сначала перейти в ключ реестра HKLM/Software/WroxPress/SelfPlacingWindow. При этом, однако, мы надеемся найти ключ, чтобы его можно было прочитать. Если его нет, то, вероятно, пример выполняется в первый раз. В этом случае мы хотим прервать чтение ключей, и, конечно, не желаем создавать какие-либо ключи. Теперь мы все время используем метод RegistryKey.OpenSubkey(). Если на каком-то этапе OpenSubkey() возвращает ссылку null, то мы знаем, что ключ реестра отсутствует, и можем вернуть значение false в вызывающий код.

В реальности для считывания ключей используется метод RegistryKey.GetValue(), который определен как возвращающий объектную ссылку, это означает, что такой метод может на самом деле вернуть экземпляр практически любого класса, который он выберет подобно SetValue(), он возвращает класс объекта, соответствующий типу данных, которые он найдет в ключе. Поэтому можно предполагать, что ключ REG_SZ будет выдан как строка, а другие ключи — как int. Мы также преобразуем соответственно возвращаемую из SetValue() ссылку. При возникновении исключения, если кто-то делал какие-то манипуляции с реестром и исказил типы данных, наше преобразование породит исключение, которое будет перехватываться обработчиком в конструкторе Form1.

Остальная часть кода использует еще один тип данных, структуру Size, выглядящую пока незнакомой, потому что она будет рассматриваться только в главе GDI+. Структура Size аналогична Point, но используется для представления размеров, а не координат. Она имеет два свойства члена — Width и Height, и мы используем структуру Size в данном случае просто как удобный способ представления размера формы для вывода в поле списка.

Заключение

В этой главе было рассмотрено использование базовых классов .NET для доступа к реестру и файловой системе из кода C#. Мы видели, что базовые классы предоставляют достаточно мощные и при этом доступные объектные модели для создания, модификации или чтения ключей в случае реестра, а в случае файловой системы — для копирования файлов, перемещения, создания и удаления файлов и папок, а так же для чтения и записи текстовых или двоичных файлов.

В этой главе предполагалось, что код должен выполняться с помощью учетной записи, которая имеет необходимые полномочия доступа для выполнения действий, требуемых кодом. Очевидно, что вопрос безопасности является важным вопросом там, где дело касается доступа к файлам, и эта область будет рассмотрена в главе 25.

Глава 15

Работа с активным каталогом

Мы получаем активный каталог (Active Directory) как часть Windows 2000 Server. Активный каталог является службой каталога, где может храниться информация о пользователях, принтерах, службах и обычные данные. Exchange Server 2000 компании Microsoft интенсивно использует его для хранения общедоступных папок и другой информации. Мы также можем хранить в активном каталоге определяемые нами данные. В файловой системе каталог хранит файлы, телефонный каталог хранит телефонные номера и имена. Служба каталога делает доступной информацию в каталоге. С помощью Проводника можно, например, находить файлы.

До появления ADS сервер Exchange мог использовать активный каталог для хранения своих объектов. Системным администраторам приходилось конфигурировать два идентификатора пользователя для одного человека: учетную запись пользователя в домене Windows NT, чтобы можно было зарегистрироваться в системе, и пользователя в Exchange Directory. Это было необходимо, так как для пользователей требовалась дополнительная информация (такая как адреса e-mail, телефонные номера и так далее), а данные о пользователях домена NT были нерасширяемыми, что не позволяло поместить туда требуемую информацию. Теперь системному администратору достаточно сконфигурировать только одного пользователя для человека в активном каталоге, данные объекта пользователя можно расширять, чтобы удовлетворить требованиям Exchange Server. Мы можем также расширить эту информацию.

Рассмотрим менеджера проекта в большой компании, который ищет с помощью Active Directory разработчика, способного создать приложения с помощью C#. Было бы неплохо, если бы менеджер мог сделать простой запрос для получения списка всех разработчиков, удовлетворяющих его требованиям. Такую возможность предоставляет активный каталог, в котором объект пользователя дополняется списком навыков.

Рассмотрим другой пример, где активный каталог может сыграть полезную роль: допустим, сконфигурирован используемый по умолчанию черно-белый принтер, но потребовалась цветная печать. Пользователь знает, что в пределах досягаемости имеется цветной принтер, который удовлетворяет требованиям, но какое у этого принтера имя? В диалоговом окне печати принтер можно выбирать из списка сотен странных имен типа Pikachu, Poliwag, Cloyster, Jynx, Staryu, которые когда-то выбрал системный администратор. Как выбрать правильный принтер? Давайте создадим решение, где пользователь может ввести такие требования, как расположение, двусторонняя печать и цвет для поиска принтера. Такая дополнительная информация о принтере также хранится в активном каталоге.

С помощью среды .NET можно легко получить доступ и манипулировать данными в службе каталога с помощью классов из пространства имен System.DirectoryServices.

Отметим, что для примеров в этой главе требуется Windows 2000 Server с установленным и сконфигурированным активным каталогом (Active Directory). После небольшой адаптации вы можете использовать классы пространства имен System.DirectoryServices, применяющиеся для службы каталогов Novell и Windows NT4.

В этой главе мы рассмотрим:

□ Архитектуру активного каталога.

□ Чтение и изменение данных в активном каталоге.

□ Поиск объектов в активном каталоге.

Архитектура активного каталога

Здесь мы рассмотрим, как работает активный каталог, для чего он используется и какие данные можно в нем хранить.

Свойства

Свойства активного каталога представлены в следующем списке:

□ Данные в активном каталоге группируются иерархически. Объекты могут храниться внутри других объектов-контейнеров. Вместо того чтобы входить в один большой список, пользователи могут группироваться внутри организационных единиц. Организационная единица включает в себя другие организационные единицы, и таким образом можно построить дерево.

□ Активный каталог использует репликацию мультимастера. В противоположность доменам Windows NT 4, где контроллер первичного домена был мастером, при использовании активного каталога все серверы являются мастерами. Если первичный контроллер домена в домене Windows NT 4 выключен, ни один пользователь не может сменить пароль; системный администратор обновляет список пользователей только в том случае, когда первичный контроллер домена включен и работает. При использовании активного каталога обновление можно делать на любом сервере. Эта модель более масштабируема, так как обновления могут происходить на различных серверах одновременно. Недостатком такой модели является большая сложность репликации. Вопросы репликации мы рассмотрим позже.

□ Топология репликации является гибкой, чтобы поддерживать репликации на медленных соединениях в WAN. Как часто должны реплицироваться данные, конфигурируется администраторами доменов.

□ Активный каталог поддерживает открытые стандарты. LDAP (Lightweight Directory Access Protocol — легковесный протокол доступа к каталогу), является одним из стандартов, который может использоваться для доступа к данным в активном каталоге. LDAP является стандартом Интернета, который может использоваться для доступа ко множеству различных служб каталога. Так как LDAP также является интерфейсом программирования, то определен LDAP API, который можно применять для доступа к активному каталогу с помощью языка C. Предпочтительный интерфейс программирования компании Microsoft для служб каталога — это ADSI (Active Directory Service Interface — интерфейс служб активного каталога). Его конечно, нельзя назвать открытым стандартом. В противоположность LDAP API с помощью ADSI можно получить доступ ко всем свойствам активного каталога. Другим стандартом, который используется в активном каталоге, является Kerberos. Kerberos используется для аутентификации. Служба Kerberos в Windows 2000 может также применяться для аутентификации клиентов Unix. Это не работает в обратную сторону; серверы Unix Kerberos не могут аутентифицировать клиентов Windows 2000, так как компания Microsoft расширила протокол Kerberos для своего собственного использования.

□ При использовании активного каталога мы имеем детально структурированную систему безопасности. Каждый объект, хранящийся в активном каталоге, имеет связанный список управления доступом, определяющий, кто и какие действия может производить с объектом.

Объекты в каталоге являются строго типизированными. Это означает, что тип объектов точно определен, к объекту нельзя добавить ни одного не специфицированного атрибута. В схеме (Schema) определены типы объектов, а также части объектов (атрибуты). Атрибуты могут быть обязательными или необязательными.

Концепции активного каталога

Прежде чем программировать для активного каталога, необходимо познакомиться с некоторыми концепциями. Существует много новых терминов, которые надо знать при разговоре об активном каталоге.

Объекты

В активном каталоге хранятся объекты. Объект — это что-то конкретное, например пользователь, принтер или общий сетевой ресурс. Объекты имеют обязательные и необязательные атрибуты, которые описывают объект. Примерами атрибутов объекта user (пользователь) являются фамилия, имя, адрес e-mail, телефонный номер и т. д.

На следующем рисунке показаны: контейнерный объект Wrox Press, несколько объектов пользователей, контакт, принтер и объект группы пользователей:

Схема

Каждый объект является экземпляром класса в схеме (schema). Схема хранится среди объектов активного каталога. Мы должны различать classSchema и attributeSchema. В classSchema определяется тип объекта, а также данные о том, какие обязательные и необязательные атрибуты имеет объект. attributeSchema определяет, на что похож атрибут, и какой допустим для него синтаксис.

Можно пользоваться собственными типами и атрибутами и добавить их к схеме. Помните, что новый тип схемы вам не удастся убрать из активного каталога. Можно пометить его как неактивный, чтобы новые объекты больше нельзя было создавать, но могут существовать объекты этого типа, поэтому невозможно удалить классы или атрибуты, которые определены в схеме. Windows 2000 Administrator не имеет достаточных полномочий для создания новых записей схемы, здесь нужен Windows 2000 Domain Enterprise Administrator.

Конфигурация

В активном каталоге хранятся не только объекты, но и определения классов (в схеме) и конфигурация. Конфигурация активного каталога хранит информацию обо всех сайтах, интервалах репликации и т.д., которая задается системным администратором. К конфигурационной информации можно получить доступ, как ко всем другим объектам в активном каталоге.

Домен активного каталога

Домен является границей безопасности сети Windows. В домене активного каталога объекты хранятся в иерархическом порядке. Сам активный каталог состоит из одного или нескольких доменов. Иерархический порядок объектов представлен на рисунке ниже. Контейнерные объекты, такие как Users, Computers и Books, могут хранить другие объекты:

На следующем рисунке мы видим домен, который представлен треугольником. Каждый овал на рисунке представляет объект, линии между объектами показывают отношения предок/потомок. Books является предком для .NET и Java. Pro C#, Beg C#, и ASP.NET являются объектами-потомками объекта .NET.

Контроллер домена

Один домен может иметь несколько серверов, каждый из которых хранит все объекты внутри домена. Не существует мастер-сервера, и все серверы интерпретируются одинаково; мы имеем модель с несколькими мастерами. Объекты реплицируются между серверами внутри домена.

На следующем рисунке домен Wrox.com представлен треугольником. DC1 и DC2 являются двумя контроллерами домена для этого домена:

Сайт

Сайт является местоположением в сети, содержащим серверы активного каталога (контроллеры домена). Если имеется несколько местоположений на предприятии, соединенных медленными сетевыми связями, то можно использовать несколько сайтов для одного домена. Для целей резервного копирования или масштабирования каждый сайт имеет один или несколько выполняющихся контроллеров доменов. Репликация между серверами на сайте может происходить с более короткими интервалами в связи с более быстрым сетевым соединением. Репликация между серверами на различных сайтах конфигурируется с большими интервалами времени, в зависимости от скорости сети. Конечно, это может сконфигурировать администратор домена.

На следующем рисунке показан один домен Wrox.com, который имеет несколько сайтов: Seattle, New York и Chicago. В каждом из этих сайтов выполняются по два контроллера домена.

Дерево домена

Несколько доменов могут соединяться доверительными отношениями. Эти домены совместно используют общую схему, общую конфигурацию и глобальный каталог. Общая схема и общая конфигурация означают, что эти данные реплицируются между доменами. Деревья доменов совместно используют одну и ту же схему классов и атрибутов. Сами объекты не реплицируются между доменами.

Домены, соединенные таким образом, называются деревом доменов. Домены в дереве доменов имеют непрерывное иерархическое пространство имен. Это означает, что имя домена-потомка добавляется к имени домена-предка. Между доменами устанавливаются доверительные отношения, использующие протокол Kerberos.

На следующем рисунке показан корневой домен wrox.com, который является также доменом-предком доменов-потомков france.wrox.com и uk.wrox.com. Доверительные отношения задаются между доменами предком и потомком так, чтобы учетные записи одного домена можно было аутентифицировать в другом:

Лес

Соединение множества деревьев доменов с помощью общей схемы, общая конфигурация и глобальный каталог без непрерывного пространства имен называется лесом. Лес является множеством деревьев доменов. Лес может иметь место, если у компании есть филиал, где должно применяться другое имя домена. Предположим, что домен asptoday.com должен быть относительно независимым от домена wrox.com, но должно быть общее управление, а также возможность пользователям из домена asptoday.com иметь доступ к ресурсам из домена wrox.com и наоборот. С помощью леса можно создать доверительные отношения между множеством деревьев доменов.

Глобальный каталог

Поиск объекта может охватывать несколько доменов. Если ищется объект определенного пользователя с некоторыми атгрибутами, то необходимо вести поиск в каждом домене. Начиная с wrox.com, поиск продолжается в uk.wrox.com и france.wrox.com через медленные соединения, такой поиск может потребовать достаточно много времени.

Чтобы ускорить поиск, все объекты копируются в глобальный каталог. Глобальный каталог реплицируется в каждый домен леса. Существует по крайней мере один сервер в каждом домене, содержащий глобальный каталог. По соображениям производительности и масштабируемости можно иметь в домене более одного сервера с глобальным каталогом. С помощью глобального каталога поиск по всем объектам может вестись на одном сервере.

На следующем рисунке мы видим три домена в дереве доменов. Каждый из этих доменов имеет глобальный каталог. По умолчанию глобальный каталог устанавливается на первом контроллере домена, но могут быть добавлены дополнительные каталоги:

Глобальный каталог является для всех объектов кэш-памятью только для чтения. Каталог может применяться лишь для поиска; для выполнения обновлений должны использоваться контроллеры доменов.

Не все атрибуты объекта хранятся в глобальном каталоге. Можно определить, должен ли он храниться там или нет. Решение о необходимости хранения атрибута в глобальном каталоге зависит от частоты использования его при поиске. Изображение пользователя бесполезно в глобальном каталоге, так как вряд ли поиск будет осуществиться по изображению. Вместо этого полезным добавлением для хранения был бы номер телефона. Атрибут можно также использовать для уточнения того, что он должен быть проиндексирован, с целью ускорения запроса этого атрибута.

Репликация

Как программисты мы не будем конфигурировать репликацию, но так как она является важным аспектом данных, которые хранятся в активном каталоге, необходимо знать, как она работает. Активный каталог использует архитектуру серверов с несколькими мастерами. Обновления могут и будут происходить на каждом контроллере домена в домене. Задержка репликации определяет, сколько времени потребуется для выполнения обновлений.

□ Конфигурируемое уведомление об изменении происходит внутри сайта по умолчанию каждые 5 минут, если изменяются некоторые атрибуты. Сервер, где происходит изменение, информирует все серверы по очереди с 30-секундным интервалом. В дальнейшем сервер может получать уведомление об изменении через 7 минут. По умолчанию уведомление об изменении между сайтами задается равным 180 минутам. Внутри- и межсайтная репликация может быть сконфигурирована на другие значения.

□ Если никаких изменений не происходит, плановая репликация выполняется каждые 60 минут внутри сайта. Это служит гарантией того, что уведомление об изменении не будет пропущено.

□ Для информации, чувствительной к безопасности, такой как блокирование учетной записи, может происходить немедленное уведомление.

Изменения при репликации копируются в контроллеры доменов. Для каждого изменения атрибута записываются номер версии (USN — номер последовательности обновления) и отметка времени. Они используются для разрешения конфликтов, если обновления происходят в одном и том же атрибуте на различных серверах.

Рассмотрим пример. Атрибут "мобильный телефон" пользователя Джона Доу имеет номер USN 47. Это значение уже реплицировано во все контроллеры доменов. Один системный администратор изменяет телефонный номер. Изменение происходит на сервере DC1, новый USN этого атрибута на сервере DC1 теперь будет 48, а остальные контроллеры доменов по прежнему имеют USN, равным 47. Если кто-то все еще читает атрибут, то он может считать старое значение, так как репликация еще не произошла на всех контроллерах доменов.

Теперь может произойти редкий случай, когда другой администратор изменяет атрибут "телефонный номер", и здесь был выбран другой контроллер домена, так как этот администратор получил более быстрый ответ от сервера DC2. USN этого атрибута на сервере DC2 также изменяется на 48.

Через заданный интервал происходит уведомление, так как USN атрибута изменился, и последний раз репликация происходила со значением USN, равным 47. С помощью механизма репликации теперь обнаруживается, что серверы DC1 и DC2 оба имеют USN, равный 48, для атрибута "номер телефона". Какой сервер будет победителем в действительности не имеет значения, но один сервер должен выиграть. Чтобы разрешить этот конфликт, используется отметка времени изменения. Так как изменение произошло позднее на DC2, то будет реплицировано значение, которое хранится в контроллере домена DC2.

При считывании объектов мы должны знать, что данные не обязательно будут актуальны. Актуальность данных зависит от задержек репликации.

При обновлении объектов другой пользователь может по-прежнему прочитать некоторые старые значения после обновления. Также возможно, что различные обновления будут происходить в одно время.

Характеристики данных активного каталога

Активный каталог не заменяет реляционную базу данных или реестр. Какие же данные могут там хранится?

□ В активном каталоге имеются иерархические данные. Мы можем иметь контейнеры, в которых опять же хранятся контейнеры, а также объекты. Сами контейнеры тоже являются объектами.

□ Данные должны использоваться в основном для чтения. Так как мы имеем репликацию, которая происходит с некоторым задаваемым интервалом времени, мы не можем быть уверены, что прочитаем своевременные данные. В приложениях необходимо учитывать, что прочитанная информация, возможно, не является реальной современной информацией.

□ Данные представляют глобальный интерес для предприятия. Добавление нового типа данных в схему реплицирует его на серверы предприятия. Если типы данных представляют интерес только для небольшого числа пользователей, то администратор домена предприятия не будет устанавливать новые типы схемы.

□ Данные должны иметь разумный размер в связи с репликацией. Если размер данных равен 100Кбайт, то имеет смысл хранить их в каталоге, если данные изменяются только раз в неделю. Данные такого размера слишком велики, если они изменяются каждый час. Всегда помните о репликации — куда будут пересылаться данные и с какими интервалами времени. Если имеется большой объем данных, то можно поместить в активный каталог ссылку, а сами данные хранить в другом месте.

Суммируя, можно сказать, что хранимые в активном каталоге данные должны быть иерархически организованны, иметь разумный размер и быть важными для предприятия.

Схема

Объекты активного каталога являются строго типизированными. Схема определяет типы объектов, обязательные и необязательные атрибуты, а также синтаксис и ограничения атрибутов. Сама схема хранится как объекты в хранилище данных активного каталога. В схеме можно различить объекты схемы классов и схемы атрибутов. Класс является совокупностью атрибутов. С помощью классов поддерживается одиночное наследование. Как можно видеть на следующей диаграмме классов, класс user является производным от класса organizationalPerson, organizationalPerson является подклассом person, а базовым классом для всех является top. Класс classSchema, который определяет класс, описывает атрибуты с помощью атрибута systemMayContain.

На диаграмме перечислены лишь немногие из всех значений systemMayContainтолько для того, чтобы передать идею конструкции. Можно легко найти все значения с помощью ADSIEdit. По корневому классу top видно, что каждый объект может иметь атрибуты общее имя (cn), displayName, objectGUID, whenChanged и whenCreated. Класс Person является производным от top. Объект Person имеет также пароль userPassword и telephoneNumber. organizationalPerson является производным от Person. В дополнение к Person он имеет manager, department, company, a user имеет атрибуты, необходимые для регистрации в системе:

Управление активным каталогом

На самом деле мы не будем говорить об управлении активным каталогом. За управление отвечают системные администраторы Windows 2000, а мы хотим поговорить о программировании активного каталога. Однако рассмотрение некоторых инструментов управления может помочь понять, что такое активный каталог, какие данных в нем находятся, и что можно сделать программным путем.

Системный администратор имеет множество инструментов для ввода новых данных, обновления данных и для конфигурирования активного каталога. С помощью инструмента Active Directory Users and Computers (Пользователи и компьютеры активного каталога) можно обновить данные о пользователях и ввести новых пользователей. Инструмент Active Directory Sites and Services используется для конфигурирования сайтов в домене и репликации между этими сайтами. ActiveDirectory Domains and Trusts может применяться для создания доверительных отношений между доменами в дереве. Редактором реестра для активного каталога, где можно просмотреть и отредактировать каждый объект, является ADSI Edit. В дополнение к инструментам системного администратора имеется инструмент в SDK платформы Microsoft: ADSI Viewer.

Active Directory Users and Computers

Утилита Active Directory Users and Computers является инструментом, который в основном служит системным администраторам для управления своими пользователями. Start|Programs|Administrative Tools|Active Directory Users and Computers:

С помощью этой утилиты можно добавлять новых пользователей, группы, контакты, организационные единицы, принтеры, общие папки, компьютеры, и модифицировать существующие. На изображении ниже можно видеть атрибуты, которые вводятся для объекта пользователь (user): офис, номера телефонов, адреса e-mail, web-страницы, информация об организации, адреса, группы и т.д., т.е. значительно больше информации, чем было возможно в домене NT 4:

Утилита Active Directory Users and Computers может также использоваться на больших предприятиях с миллионами объектов. Не обязательно просматривать список с тысячами объектов, можно выбрать специальный фильтр, чтобы выводились только некоторые объекты. Можно также сделать запрос LDAP для поиска объектов на предприятии.

ADSI Edit

ADSI Edit является редактором реестра активного каталога. Эта утилита не устанавливается автоматически. На компакт-диске Windows 2000 Server можно найти каталог с именем Supporting Tools. Когда утилиты поддержки будут установлены, ADSI Edit можно будет найти в меню: Start|Programs|Windows 2000 Support Tools|Tools|ADSI Edit.

Простая в использовании утилита Active Directory Users and Computers имеет фиксированный интерфейс пользователя для изменения атрибутов объектов пользователя. Мы не увидим атрибутов, которые добавляются к схеме в интерфейсе пользователя этой утилиты, управляемом мышью. Все можно сконфигурировать с помощью ADSI Edit, мы можем также просмотреть схему и конфигурацию. Эта утилита, однако, не так проста в использовании, и очень легко ввести неправильные данные:

Открывая окно Properties (Свойства) объекта, можно изменить любой атрибут объекта в активном каталоге. Мы видим обязательные, дополнительные атрибуты, типы и значения атрибутов:

ADSI Viewer

Установим также браузер активного каталога как часть SDK платформы Microsoft. SDK платформы Microsoft не является частью дистрибутива Visual Studio.NET. Вы получаете компакт-диск с подпиской на MSDN или загружаете его с web-сайта MSDN. После установки SDK платформы можно запустить утилиту с помощью пункта меню Start|Programs|Microsoft Platform SDK|Tools|ADSI Viewer.

ADSI Viewer имеет два режима. С помощью File|New можно запустить запрос или использовать Object Viewer для вывода и изменения атрибутов объектов. После запуска Object Viewer можно определить путь доступа LDAP, а также имя пользователя и пароль, чтобы открыть объект. Вскоре, когда мы начнем делать это программным путем, вы увидите, какую форму может иметь путь доступа LDAP. Здесь определяется LDAP://OU=Wrox Press, DC=eichkogelstrasse, DC=local, чтобы получить доступ к объекту организационной единицы:

Если определяемый объект с путем доступа и именем пользователя и паролем является допустимым, мы получаем экран Object Viewer где можно видеть и изменять свойства объекта и объектов-потомков:

Интерфейсы службы активного каталога (ADSI)

Интерфейсы службы активного каталога (ADSI) являются программным интерфейсом к службам каталога. ADSI определяет некоторые интерфейсы COM, которые реализуются провайдерами ADSI. Это означает, что клиент может использовать различные службы каталога с одними и теми же программными интерфейсами. Классы среды .NET в пространстве имен System.DirectoryServices используют интерфейсы ADSI.

На следующем рисунке можно видеть некоторых провайдеров ADSI (LDAP, WinNT, NDS), которые реализуют интерфейсы COM, такие как IAD и IUnknown. Сборка System.DirectoryServices использует провайдеров ADSI:

Программирование активного каталога

Чтобы разрабатывать программы для активного каталога, мы используем классы из пространства имен System.DirectoryServices. С помощью этих классов можно запрашивать объекты, просматривать и обновлять свойства, а также вести поиск объектов.

В следующих сегментах кода простое консольное приложение C# демонстрирует применение классов в пространстве имен System.DirectoryServices.

Классы в System.DirectoryServices

Следующая таблица показывает основные классы в пространстве имен System.DirectoryServices:

Класс

Описание

DirectoryEntry

Этот класс является основным классом в пространстве имен System.DirectoryServices. Объект этого класса представляет объект в хранилище активного каталога. Мы используем этот класс для связывания с объектом, просмотра и обновления свойств. Свойства объекта представлены в PropertyCollection. Каждый элемент в PropertyCollection имеет в свою очередь PropertyValueCollection.

DirectoryEntries

DirectoryEntries является коллекцией объектов DirectoryEntry. Свойство Children объекта DirectoryEntry возвращает список объектов в коллекцию DirectoryEntries.

DirectorySearcher

Этот класс является основным классом, используемым для поиска объектов со специфическими атрибутами. Чтобы определить поиск, можно использовать класс SortOption и перечисления SearchScope, SortDirection и ReferalChasingOption. Результаты поиска находятся в классе SearchResult или SearchResultCollection. Мы также получаем объекты ResultPropertyCollection и ResultPropertyValueCollection.

Связывание

Чтобы получить значения объекта в активном каталоге, мы должны соединиться с активным каталогом. Процесс соединения называется связыванием. Путь доступа связывания может выглядеть следующим образом:

LDAP://dc01.globalknowledge.net/OU=Marketing, DC=GlobalKnowledge, DC=Com

Во время процесса связывания можно определить следующие позиции:

□ Протокол, определяющий используемого провайдера.

□ Имя сервера контроллера домена.

□ Номер порта серверного процесса.

□ Известное имя объекта для идентификации объекта, к которому требуется доступ.

□ Имя пользователя и пароль, если требуется другой пользователь для доступа к активному каталогу.

□ Можно также определить тип аутентификации, если требуется шифрование.

Протокол

Первая часть пути связывания определяет провайдера ADSI. Провайдер реализован как сервер COM; для идентификации можно найти идентификатор программы прямо в ключе HKEY_CLASSES_ROOT. Провайдеры, которых получают вместе с Windows 2000, перечислены в следующей таблице:

Протокол

Описание

LDAP

Сервер LDAP, такой как каталог Exchange и сервер активного каталога Windows 2000.

GC

GC применяется для доступа к глобальному каталогу в активном каталоге. Он может использоваться для быстрых запросов.

IIS

С помощью провайдера ADSI для IIS можно создавать новые web-сайты в каталоге IIS.

WinNT

Чтобы получить доступ к базе данных пользователей старых доменов Windows NT 4, можно использовать провайдера ADSI для WinNT, Факт, что пользователи NT 4 имеют только несколько атрибутов, остается неизменным.

NDS

Этот идентификатор программы используется для коммуникации со службами каталогов Novell.

NWCOMPAT

С помощью NWCOMPAT можно получить доступ к старым каталогам Novell: Novell Netware 3.x.

Имя сервера

Имя сервера следует за протоколом в строке соединения. Имя сервера является не обязательным, если вы зарегистрировались в домене активного каталога. Без имени сервера происходит связывание без сервера; это означает, что Windows 2000 пытается получить "лучший" контроллер домена в домене, который ассоциирован с пользователем, выполняющим связывание. Если внутри сайта нет сервера, будет использоваться первый найденный контроллер домена.

Связывание без сервера может выглядеть следующим образом: LDAP://OU=Sales, DC=GlobalKnowladge, DC=Com.

Номер порта

После имени сервера можно определить номер порта серверного процесса, используя синтаксис :xxx. По умолчанию для сервера LDAP используется номер порта 389: LDAP://dc01.globalknowledge.net:389. Сервер Exchange использует тот же самый номер порта, что и сервер LDAP. Если сервер Exchange установлен на той же системе, например, как контроллер домена активного каталога, то можно сконфигурировать другой порт.

Известное имя

Четвертая часть, которую мы должны определить в пути доступа,— это известное имя (DN — Distinguished Name). Известное имя является гарантированным уникальным именем, идентифицирующим объект, к которому требуется доступ. В активном каталоге для определения имени объекта можно использовать синтаксис LDAP, который основывается на X.500.

Известное имя

CN=Christian Nagel, OU=Trainer, DC=GlobalKnowledge, DC=com

определяет общее имя (CN) Christian Nagel в организационной единице (OU) Trainer в компоненте домена (DC) GlobalKnowledge домена GlobalKnowledge.com. Часть, которая определена самой правой является корневым объектом домена. Имя должно следовать иерархии дерева объектов.

Спецификацию LDAP для строкового представления известных имен можно найти в RFC 2253: www.ietf.org/rfc/rfc2253.txt.

Относительное известное имя

Относительное известное имя (RDN) используется для ссылки на объект внутри контейнерного объекта. Для RDN спецификации OU и DC не требуются, будет достаточно общего имени. CN=Christian Nagel является относительным известным именем внутри организационной единицы. Относительное известное имя может использоваться, если мы уже имеем ссылку на объект контейнера и хотим получить доступ к объектам-потомкам.

Используемый по умолчанию именующий контекст

Если известное имя не определено в пути доступа, процесс связывания будет выполняться в используемом по умолчанию именующем контексте. Можно считать используемый по умолчанию именующий контекст с помощью rootDSE. LDAP 3.0 определяет rootDSE как корень дерева каталогов на сервере каталога.

LDAP://rootDSE или LDAP://servername/rootDSE

Перечисляя все свойства rootDSE, можно получить информацию о defaultNamingContext, который будет использоваться, когда не определено никакое имя. schemaNamingContext и configurationNamingContext определяют требуемые имена, которые будут использоваться для доступа к схеме и конфигурации в хранилище активного каталога.

Следующий код используется для получения всех свойств rootDSE. Речь идет о связывании с помощью класса DirectoryEntry:

using (DirectoryEntry de = new DirectoryEntry()) {

de.Path = "LDAP://celtlcrain/rootDSE";

de.Username = @"sentinel\chris";

de.Password = "mausemaus3";

PropertyCollection props = de.Properties;

foreach (string prop in props.PropertyNames) {

PropertyValueCollection values = props[prop];

foreach (string val in values) {

Console.Write(prop + ": ");

Console.WriteLine(val);.

}

}

}

Помимо других свойств результат вывода этой программы показывает defaultNamingContext DC=eichkogelstrasse, DC=local, контекст, который можно использовать для доступа к схеме: CN=Schema, CN=Configuration, DC=eichkogelstrasse, DC=local и именующий контекст конфигурации: CN=Configuration, DC=eichkogelstrasse, DC=local:

Идентификатор объекта

Каждый объект имеет уникальный идентификатор — GUID. GUID является уникальным 128-битовым числом. Мы можем с соединиться с объектом, используя GUID. Таким образом, мы всегда получаем тот же самый объект, даже если объект был перемещен в другой контейнер. GUID генерируется при создании объекта и всегда остается тем же самым.

Можно получить строковое представление GUID с помощью DirectoryEntry.NativeGuid. Затем это строковое представление можно использовать для соединения с объектом. Даже если объект перемещается в другой контейнер, мы всегда получаем тот же объект.

Следующий пример показывает имя пути доступа для связывания без сервера со специфическим объектом, представленным GUID:

LDAP://<GUID=14abbd652aae1a47abc60782dcfc78ea>

Имена объектов в доменах Windows NT

Провайдер WinNT не допускает синтаксис LDAP в части имени строки связывания. Для этого провайдера объект определяется с помощью ObjectName, ClassName. Действительные строки связывания для домена Windows NT имеют следующий вид:

WinNT:

WinNT://DomainName

WinNT://DomairName/UserName, user

WinNT://DomainName/dc01/MyGroup, group

Имя пользователя

Если другой пользователь, отличный от зарегистрированного, должен использоваться для доступа к каталогу, так как зарегистрированный пользователь не имеет требуемых полномочий для доступа к активному каталогу, то должны определяться явные полномочия пользователя для процесса связывания. Для активного каталога имеется ряд способов задания имени пользователя.

Низкоуровневая регистрация

Для низкоуровневой регистрации имя пользователя можно определить с помощью имени домена:

domain\username

Известное имя

Пользователя можно определить также с помощью известного имени объекта пользователя, например:

CN=Administrator, CN=Users, DC=eichkogelstrasse, DC=local

Имя пользователя принципала (UPN)

UPN объекта определяется с помощью атрибута userPrincipalName. Системный администратор определяет его по информации регистрации на вкладке Account свойств User с помощью утилиты Active Directory Users and Computers. UPN не является адресом e-mail пользователя

Эта информация также уникальным образом определяет пользователя и может использоваться для регистрации:

Nagel@eichkogestrasse.local

Аутентификация

Для безопасной зашифрованной аутентификации можно также определить тип аутентификации. Аутентификация может задаваться с помощью свойства AuthenticationType класса DirectoryEntry. При этом присваиваемое значение является одним из перечислений AuthenticationTypes.

Связывание с помощью класса DirectoryEntry

Класс System.DirectoryServices.DirectoryEntry используется для определения всех связываемых данных. Можно использовать конструктор по умолчанию и определить данные связывания со свойствами Path, Username, Password и AuthenticationType или передать всю информацию в конструктор:

using (DirectoryEntry de = new DirectoryEntry()) {

de.Path = "LDAP://celticrain/DC=eichkogelstrasse, DC=local";

de.Username = "nagel@eichkogelstrasse.local";

de.Password = "someSecret";

// использовать полномочия текущего пользователя

DirectoryEntry de2 = new DirectoryEntry("LDAP://DC=eichkogelstrasse, DC=local");

Даже если создание объекта DirectoryEntry пройдет успешно, это не означает, что и связывание было успешным. Связывание произойдет, когда в первый раз будет прочитано свойство во избежание ненужного сетевого трафика. Существует ли объект или правильны ли полномочия определенного пользователя, можно будет увидеть при первом доступе к объекту.

Получение записей каталога

Теперь, когда мы знаем, как определить атрибута связывания с объектом в активном каталоге, давайте прочитаем атрибуты объекта.

Свойства объектов пользователей

Класс DirectoryEntry имеет некие свойства Name, Guid и SchemaClassName для получения информации об объекте. Первый раз при доступе к свойству объекта DirectoryEntry происходит связывание и заполняется кэш. Когда мы обращаемся к другому свойству, мы считываем его из кэша, и коммуникации с сервером не требуется для данных из того же объекта.

В следующем примере мы обращаемся к объекту пользователя с общим именем Christian Nagel в организационной единице Wrox Press.

DirectoryEntry de = new DirectoryEntry();

de.Path = "LDAP://celticrain/CN=Christian Nagel, " +

"OU=Wrox Press, DC=eichkogelstrasse, DC=local";

Console.WriteLine("Name: "+ de.Name);

Console.WriteLine("GUID: " + de.Guid);

Console.WriteLine("Type: " + de.SchemaClassName);

Console.WriteLine();

Объект активного каталога содержит значительно больше информации. Доступность информации зависит от типа объекта. Чтобы получить всю информацию об объекте, свойство Properties возвращает PropertyCollection. Каждое свойство само является коллекцией, так как одно свойство может иметь несколько значений, например, объект пользователя может иметь несколько телефонных номеров. Мы перебираем значения с помощью внутреннего цикла foreach. Коллекция, которая возвращается из properties[name] является массивом объектов. Значения атрибутов могут быть строками, числами или другими типами данных. Мы используем метод ToString() для вывода значений.

Console.WriteLine("Attributes: ");

PropertyCollection properties = de.Properties;

foreach (string name in properties.PropertyNames) {

foreach (object о in properties[name]) {

Console.WriteLine(name + ": " + o.ToString());

}

}

В выходных результатах мы видим все атрибуты объекта пользователя Christian Nagel. Заметим, что otherTelephone является многозначным свойством, которое содержит несколько телефонных номеров. Некоторые из значений свойств просто выводят тип объекта System._ComObject. Чтобы получить значения этих атрибутов, необходимо непосредственно использовать интерфейсы ADSI COM, которые также берутся из классов в пространстве имен System.DirectoryServices.

В главе 19 можно прочитать, как работать с объектам и и интерфейсами COM.

Для получения дополнительной информации об ADSI можно прочитать книгу Simon Robinson, Professional ADSI Programming, Wrox Press, ISBN 1-861002-26-2.

Доступ к свойствам непосредственно по имени

С помощью DirectoryEntry.Properties можно получить доступ ко всем свойствам. Если имя свойства известно, можно получить значение непосредственно:

foreach (string homePage in de.Properties["wWWHomePage"])

Console.WriteLine("Home page; " + homePage);

Коллекции объектов

Объекты хранятся в активном каталоге иерархически. В контейнерных объектах содержатся объекты-потомки. Их можно перечислить с помощью свойства Children класса DirectoryEntry. В другом направлении можно получить контейнер объекта с помощью свойства Parent.

Объект пользователя не имеет потомков, поэтому воспользуемся теперь организационной единицей. Давайте получим все объекты пользователей из организационной единицы Wrox Press в домене eichkogelstrasse.local. Свойство Children возвращает коллекцию DirectoryEntries, которая содержит объекты DirectoryEntry. Мы просматриваем все объекты DirectoryEntry для вывода имен объектов-потомков:

DirectoryEntry de = new DirectoryEntry();

de.Path. = "LDAP://celticrain/OU=Wrox Press, " + "DC=eichkogelstrasse, DC=local";

Console.WriteLine("Children of " + de.Name);

foreach (DirectoryEntry obj in de.Children) {

Console.WriteLine(obj.Name);

}

В данном примере мы видим все объекты в организационной единице: пользователей, контакты, принтеры, общие ресурсы и другие организационные единицы. Если нужно увидеть только некоторые типы объектов, можно использовать свойство SchemaFilter класса DirectoryEntries:

DirectoryEntry de = new DirectoryEntry();

de.Path = "LDAP://celticrain/OU=Wrox Press, " + "DC=eichkogelstrasse, DC-local";

Console.WriteLine("Children of " + de.Name);

de.Children.SchemaFilter.Add("user");

foreach(DirectoryEntry obj in de.Children) {

Console.WriteLine(obj.Name);

}

В результате мы видим в организационной единице только объекты пользователей:

Кэш

Чтобы уменьшить сетевой трафик, ADSI использует кэш для свойств объектов. Как было показано ранее, обращение к серверу не происходит при создании объекта DirectoryEntry, а происходит, когда впервые считывается значение из хранилища каталога. При считывании первого свойства все свойства записываются в кэш, поэтому повторное обращение к серверу не нужно, когда считывается следующее свойство. Этот кэш свойств может быть выключен при задании свойства DirectoryEntry.UsePropertyCache как false. Лучше этого не делать, так как это будет порождать множество ненужных обращений к серверу.

Запись изменений в объекты также происходит только в кэше. Задание множества свойств не генерирует сетевого трафика. Метод DirectoryEntry.CommitChanges() требуется для очистки кэша и переноса всех измененных данных на сервер. Чтобы снова получить вновь записанные данные из хранилища каталога, можно для чтения свойств использовать метод DirectoryEntry.RefreshCache(). Задание свойства UsePropertyCache как false может быть очень полезно для отладки, чтобы увидеть, какое свойство было изменено неправильно.

Обновление записей каталога

Объекты в активном каталоге обновляются так же легко, как и читаются. Изменение значений возможно после считывания объекта. Чтобы удалить все значения одного свойства, может вызываться метод PropertyValueCollection.Clear(). С помощью метода Add() к свойству могут добавляться новые значения. Remove() и RemoveAt() удаляют специфические значения из коллекции свойства:

using (DirectoryEntry de = new DirectoryEntry!)) {

de.Path =

"LDAP://celticrain/CN=Christian Nagel, " +

"OU=Wrox Press, DC=eichkogelstrasse, DC=local";

if (de.Properties.Contains("mobile")) {

de.Properties["mobile"][0] = "+43 (664) 3434343434";

}

de.CommitChanges();

}

Чтобы изменить значение, зададим ему определенное значение. Посредством следующей строки кода для номера мобильного телефона задается новое значение, если оно существует, с использованием индекса PropertyValueCollection. С помощью индекса значение может только изменяться. Поэтому необходимо всегда проверять методом DirectoryEntry.Properties.Contains(), доступен ли атрибут:

de.Properties["mobile"][0] = "+43 (664) 3434343434";

He забудьте вызвать метод DirectoryEntry.CommitChanges() после создания или обновления новых объектов каталога. Иначе обновляется только кэш, а изменения не посылаются службе каталога.

Создание новых объектов

Новые объекты активного каталога, такие как пользователи, компьютеры, принтеры, контакты и другие программным путем можно создать с помощью класса DirectoryEntries.

Чтобы добавить новые объекты в каталог, мы должны сначала соединиться с объектом-контейнером, подобным организационной единице, куда можно вставить новые объекты. Объекты, которые не могут содержать другие объекты, использовать нельзя. Здесь используется контейнерный объект с известным именем CN=Users, DC=eichkogelstrasse, DC=local:

DirectoryEntry de = new DirectoryEntry();

de.Path = "LDAP://celticrain/CN=Users, " +

"DC=eichkogelstrasse, DC=local";

Можно получить доступ к объекту DirectoryEntries с помощью свойства Children объекта DirectoryEntry:

DirectoryEntries users = de.Children;

Объект DirectoryEntries имеет методы для добавления, удаления, и поиска объектов в коллекции. Здесь создается новый объект пользователя. Для метода Add() нам нужно имя объекта и имя типа. Можно легко получить имена типов с помощью ADSI Edit.

DirectoryEntry user = users.Add("John Doe", "user");

Объект теперь имеет значения свойств по умолчанию. Чтобы присвоить специальные значения свойств, можно добавить свойства с помощью метода Add() свойства Properties. Конечно, все свойства должны существовать в схеме для объекта пользователя. Если определенное свойство не существует, то возникнет исключение COMException "The specified directory service attribute or value doesn't exist" ("Указанный атрибут или значение службы каталога не существует"). Если имена атрибутов правильны, но сервер отказывает во входе в связи с незаконным паролем или пропущенным свойством, исключение COMException будет содержать сообщение "The server is unwilling to process the request" ("Сервер не желает обрабатывать запрос").

user.Properties["company"].Add("Some Company");

user.Properties["department"].Add("Sales");

user.properties["employeeID"].Add("4711");

user.Properties["samAccountName"].Add("John Doe");

user.Properties["userPassword"].Add("someSecret");

В данный момент не все данные записаны в активный каталог. Необходимо очистить кэш:

user.CommitChanges();

Поиск в активном каталоге

Большую часть времени активный каталог будет использоваться для поиска некоторых значений. Это хранилище данных, которое оптимизировано в основном для чтения, поэтому данные будут в основном считываться.

Для поиска в активном каталоге платформа .NET имеет класс DirectorySearcher.

Мы можем использовать поиск только с провайдером LDAP. DirectorySearcher не работает с провайдерами NDS или IIS.

В конструкторе класса DirectorySearcher существуют четыре важные части поиска. Можно также использовать конструктор по умолчанию и определять параметры поиска с помощью свойств.

SearchRoot

Корень поиска (SearchRoot) определяет, где должен начаться поиск. По умолчанию SearchRoot является корнем домена, который используется в данный момент. SearchRoot определен с помощью свойства Path объекта DirectoryEntry.

Filter

Фильтр (Filter) определяет значения, которые мы хотим найти. Фильтр является строкой, которая должна быть заключена в круглые скобки.

Операторы отношений, такие как <=, =, >=, в выражениях допускаются. (objectClass = contact) будет искать все объекты типа contact; (lastName>=Nagel) ищет все объекты, где свойство lastName равно или больше Nagel, что означает, что оно следует за ним в алфавитном порядке.

Выражения могут комбинироваться с префиксными операциями & и |. (&(objectClass=user)(description=Auth*)) ищет все объекты типа user, где свойство description начинается со строки Auth. Так как операторы & и | находятся в начале выражения, то с помощью одного префиксного оператора можно комбинировать более двух выражений.

По умолчанию используется фильтр (objectClass=*), поэтому все объекты допустимы. Синтаксис фильтра определен в RFC 2254, "Строковое представление фильтров поиска LDAP". Этот RFC можно найти по адресу www.ietf.org/rfc/rfc2254.txt.

PropertiesToLoad

С помощью PropertiesToLoad мы определяем коллекцию StringCollection всех интересующих нас свойств. Как вы уже видели, объекты могут иметь множество свойств. Большинство из них будут не важны для нашего запроса поиска. Мы определяем свойства, загружаемые в кэш. Свойствами по умолчанию, которые мы получаем, если ничего не определено, являются Path и Name для объекта.

SearchScope

SearchScope является перечислением, которое определяет, как глубоко должен распространяться поиск:

□ SearchScope.Base ищет атрибуты только в том объекте, где начинается поиск, поэтому мы получаем максимум один объект.

□ Для SearchScope.OneLevel поиск продолжается в коллекции-потомке базового объекта. Сам базовый объект для поиска не используется.

□ SearchScope.Subtree определяет, что поиск должен спускаться вниз по всему дереву.

По умолчанию для SearchScope используется Subtree.

Пределы поиска

Такой поиск может охватывать несколько доменов. Чтобы ограничить поиск некоторым числом объектов или требуемым временем, необходимо определить несколько дополнительных свойств.

Свойства DirectorySearcher

Описание

ClientTimeout

Максимальное время, в течение которого клиент ожидает, что сервер вернет результат. Если сервер не отвечает, то никаких записей не возвращается.

PageSize

При постраничном поиске сервер возвращает число объектов, определенных с помощью PageSize, а не весь результат. Это сокращает и время клиента для получения первого ответа, и необходимую память. Сервер посылает клиенту cookie, которое отправляется назад на сервер с запросом следующего поиска, чтобы поиск можно было продолжить в точке, где он закончился.

ServerPageTimeLimit

Это значение определяет время для постраничного поиска, чтобы вернуть число объектов, которое определено значением PageSize. Если время истекает до достижения значения PageSize, найденные до этого момента объекты возвращаются клиенту. Значение по умолчанию равно -1, что означает бесконечность.

ServerTimeLimit

Определяет максимальное время, в течение которого сервер будет искать объекты. Когда это время истекает, все найденные до этого момента объекты возвращаются клиенту. По умолчанию используется 120 секунд, и нельзя задать время поиска больше этого значения.

ReferalChasing

Поиск может распространяться на несколько доменов. Если корень, который определен в SearchRoot, является родительским доменом или корень не был определен, поиск может распространиться на домены-потомки. С помощью этого свойства можно определить, что поиск должен продолжаться на других серверах. ReferalChasingOption.None означает, что поиск не продолжается на другие серверы. С помощью значения ReferalChasingOption.Subordinate можно определить, что поиск должен переходить на домены-потомки. Когда поиск начинается в DC=Wrox, DC=COM, сервер возвращает множество результатов и ссылку на DC=France, DC=Wrox, DC=COM. Клиент может продолжить поиск в поддомене. ReferalChasingOption.External означает, что сервер может направить клиента на независимый сервер, которого нет в поддомене. Это вариант поведения по умолчанию. Для ReferalChasingOption.All возвращаются ссылки на внешние домены и подчиненные домены.

В рассматриваемом примере поиска мы хотим найти все объекты пользователей в организационной единице Wrox Press, где свойство description содержит значение Author.

Сначала мы соединяемся с организационной единицей Wrox Press. Здесь начинается поиск. Создадим объект DirectorySearcher, где задан SearchRoot. Фильтр определяется как (&(objectClass=user)(description=Auth*)) для того, чтобы мы нашли все объекты типа user, где свойство description начинается с последовательности Auth, за которой может следовать что-то еще. Область поиска должна быть поддеревом, чтобы поиск происходил в порождаемых организационных единицах для Wrox Press:

DirectoryEntry de new DirectoryEntry();

de.Path = "LDAP://OU=Wrox Press, " + "DC=eichkogelstrasse, DC=local";

DirectorySearcher searcher = new DirectorySearcher();

searcher.SearchRoot  = de;

searcher.Filter = "(&(objectClass=user)(description=Auth*))";

searcher.SearchScope = SearchScope.Subtree;

В результате поиска мы хотим получить свойства name, description, givenName, и wWWHomePage.

searcher.PropertiesToLoad.Add("name");

searcher.PropertiesToLoad.Add("description");

searcher. PropertiesToLoad.Add("givenName");

searcher.PropertiesToLoad.Add("wWWHomePage");

Мы готовы начать поиск. Однако, результат необходимо отсортировать. DirectorySearcher имеет свойство Sort, где можно задать SortOption. Первый аргумент конструктора SortOption определяет свойство, по которому будет проводиться сортировка, второй аргумент определяет направление сортировки. Перечисление SortDirection имеет значения Ascending и Descending.

Чтобы начать поиск, можно использовать метод FindOne() для нахождения первого объекта или FindAll(), чтобы найти все объекты. FindOne() вернет простой SearchResult, FindAll() вернет SearchResultCollection. Мы хотим получить всех авторов, поэтому используем FindAll():

searcher.Sort = new SortOption("givenName", SortDirection.Ascending);

SearchResultCollection Results = searcher.FindAll();

С помощью цикла foreach мы получаем доступ ко всем SearchResult в SearchResultCollection. SearchResult представляет один объект в кэше поиска. Свойство Properties возвращает ResultPropertyCollection, где мы получаем доступ ко всем свойствам и значениям по имени свойства и по индексу.

SearchResultCollection results = Searcher.FindAll();

foreach (SearchResult result in results) {

ResutPropertyCollection props = result.Properties;

foreach (string propName in props.PropertyNames) {

Console.Write(propName + ": ");

Console.WriteLine(props[propName][0]);

}

Console.WriteLine();

}

}

Если необходимо получить весь объект после поиска, то это также возможна. SearchResult имеет метод GetDirectoryEntry(), который возвращает соответствующую запись DirectoryEntry найденного объекта.

Результирующий вывод показывает начале списка всех авторов книги Professional C# с выбранными свойствами

Поиск объектов пользователей

Последнее приложение, которое будет создано в этой главе, это приложение Windows Forms. С его помощью можно найти все объекты пользователей домена с динамически определяемой строкой фильтра. Можно также задать свойства объектов пользователей, которые должны выводиться.

Интерфейс пользователя

Интерфейс пользователя выводит нумерованные шаги, помогая использовать приложение.

1. На первом шаге можно ввести имя пользователя, пароль и контроллер домена. Вся эта информация является необязательной. Если контроллер домена не вводится, то соединение работает со связыванием без сервера. Если отсутствует имя пользователя, то используется контекст безопасности текущего пользователя.

2. С помощью кнопки все имена свойств объекта User могут загружаться динамически в окно списка ListBoxProperties.

3. После загрузки имен свойств, можно выбрать свойства, которые должны выводиться. Режим SelectionMode окна списка задач как MultiSimple.

4. Можно ввести фильтр для ограничения поиска. Значение по умолчанию, которое задается в этом диалоговом окне, ищет все объекты пользователей: (objectClass=user).

5. Теперь можно начать поиск.

Получение именующего контекста схемы

Это приложение имеет только два метода обработки событий: первый метод — обработчик для кнопки загрузки свойств и второй — для запуска поиска в домене. В первой части мы динамически считываем свойства класса User из схемы для вывода его в интерфейсе пользователя.

В методе-обработчике buttonLoadProperties_Click() с помощью метода SetLogonInformation() имя пользователя, пароль и имя хоста считываются во время диалога и сохраняются в членах класса. Затем метод SetNamingContext() задает имя LDAP схемы и имя LDAP используемого по умолчанию контекста. Имя LDAP этой схемы используется в вызове SetUserProperties() для задания свойств в окне списка:

private void buttonLoadProperties_Click(object sender, System.EventArgs e) {

try {

SetLogonInformation();

SetNamingContext();

SetUserProperties(schemaNamingContext);

} catch (Exception ex) {

MessageBox.Show("Cheek your inputs! " + ex.Message);

}

}

protected void SetLogonInformation() {

username =

(textBoxUsername.Text == "" ? null :

textBoxUsername.Text);

password =

(textBoxPassword.Text == "" ? null :

textBoxPassword.Text);

hostname = textBoxHostname.Text;

if (hostname ! = "") hostname += "/";

}

Во вспомогательном методе SetNamingContext() мы используем корень дерева каталога для получения свойств сервера. Мы заинтересованы в значениях двух свойств: SchemaNamingContext.

protected string SetNamingContext() {

using (DirectoryEntry de = new DirectoryEntry()) {

string path = "LDAP://" + hostname + "/rootDSE";

de.Username = username;

de.Password = password;

de.Path = path;

schemaNamingContext =

de.Properties["schemaNamingContext"][0].ToString();

defaultNamingContext =

de.Properties["defaultNamingContext"][0].ToString();

}

}

Получение имен свойств класса пользователя

У нас есть имя LDAP для доступа к схеме. Можно использовать его для доступа к каталогу и для считывания свойств. Мы заинтересованы не только в свойствах класса User, но также в свойствах базовых классов для User: Organizational-Person, Person и Top. В этой программе имена базовых классов жестко закодированы. Можно было бы прочитать базовый класс динамически с помощью атрибута subClassOf. Метод GetSchemaProperties() возвращает строковый массив со всеми именами свойств определенного типа объектов. Все имена свойств собраны в объекте properties типа StringCollection:

protected void SetUserProperties(string schemaNamingContext) {

StringCollection properties = new StringCollection();

string[] data = GetSchemaProperties(schemaNamingContext, "User");

properties.AddRange(GetSchemaProperties(schemaNamingContext, "Organizational-Person"));

properties.AddRange(GetSchemaProperties(schemaNamingContext, "Person"));

properties.AddRange(GetSchemaProperties(schemaNamingContext, "Top"));

listBoxProperties.Items.Clear();

foreach (string s in properties) {

listBoxProperties.Items.Add(s);

}

}

В методе GetSchemaProperties() мы снова обращаемся к активному каталогу. В этот раз вместо rootDSE используется имя LDAP в схеме, которое мы обнаружили ранее. Свойство systemMayContain содержит коллекцию всех атрибутов, которые допустимы в классе objectType:

protected string[] GetSchemaProperties(string schemaNamingContext, string objectType) {

string [] data;

using (DirectoryEntry de = new DirectoryEntry()) {

de.Username = username;

de.Password = password;

de.Path = "LDAP://" + hostname + "/CN=" + objectType + "," + schemaNamingContext;

DS.PropertyCollection properties = de.Properties;

DS.PropertyValueCollection values = properties["systemMayContain"];

data = new String[values.Count];

values.CopyTo(data, 0);

}

return data;

}

Одно интересное замечание к этому коду: в приложении Windows Forms класс PropertyCollection пространства имен System.DirectoryServices имеет конфликт имен с System.Data.PropertyCollection. Поскольку писать такие длинные имена как System.DirectoryServices.PropertyCollection не всегда хочется, то с целью разрешения конфликта имя пространства имен можно сократить с помощью

namespace DS = System.DirectoryServices;

Именно отсюда появляется DS.PropertyCollection.

Шаг 2 приложения завершен. Окно списка (listbox) содержит все имена свойств объектов User.

Поиск объектов User

Обработчик для кнопки поиска вызывает вспомогательный метод FillResult():

private void buttonSearch_Click(object render, System.EventArgs e) {

try {

FillResult();

} catch (Exception ex) {

MessageBox.Show("Check your input: " + ex.Message)

}

}

В методе FillResult() выполняется обычный поиск в полном домене активного каталога, как мы видели раньше. SearchScope задается как Subtree, Filter для строки мы получаем из TextBox, а свойства, которые должны быть загружены в кэш, задаются значениями, которые пользователь выбирает в окне списка

protected void FillResult() {

using (DirectoryEntry root = new DirectoryEntry()) {

root.Username = username;

root.Password = password;

root.Path = "LDAP://" + hostname + defaultNamingContext;

using (DirectorySearcher searcher = new DirectorySearcher()) {

seacher.SearchRoot = root;

searcher.SearchScope = SearchScope.Subtree;

searcher.Filter = textboxfilter.Text;

searcher.PropertiesToLoad.AddRange(GetProperties());

SearchResultCollection results = searcher.FindbAll();

StringBuilder summary = new StringBuilder();

foreach (SearchResult result in results) {

foreach (string propName in result.Properties.PropertyNames) {

foreach (string s in result.Properties[propName]) {

summary.Append(" " + propName + ": " + s + "\r\n");

}

}

summary.Append("\r\n");

}

textBoxResults.Text = summary.ToString();

}

}

}

Запустив приложение, мы получим список всех объектов, которые прошли через фильтр:

Заключение

В этой главе рассматривалась архитектура активного каталога, важные концепции о доменах, деревьях и лесах. Теперь мы можем получить информацию обо всем предприятии. Создавая приложения, которые обращаются к активному каталогу, необходимо знать, что данные, которые мы считываем, возможно, не являются современными в связи с задержкой репликации.

С помощью классов из пространства имен System.DirectoryServices мы получаем простые способы доступа к активному каталогу для провайдеров ADSI. Класс DirectoryEntry делает возможным чтение и запись объектов непосредственно в хранилище данных.

С помощью класса DirectorySearcher можно выполнять сложный поиск и определять фильтры, задержки времени, свойства для загрузки, и область действия. Используя глобальный каталог, мы ускоряем поиск объектов по всему предприятию так как он хранит версию только для чтения всех объектов леса.

Глава 16

Страницы ASP.NET

Для новичков в мире C# и .NET может показаться странным, почему в книгу включена глава, посвященная ASP.NET. Это совершенно новый язык, не так ли? Не совсем. Фактически, как мы увидим, можно использовать C# для создания страниц ASP.NET. Но мы забегаем вперед. Прежде всего необходимо обсудить, что же такое ASP.NET.

ASP.NET (Active Server Pages.NET — активные серверные страницы .NET), поставляется как часть платформы .NET и является технологией, которая позволяет динамически создавать документы на сервере Web, когда они запрашиваются через HTTP. Это в основном документы HTML, хотя в равной степени можно создавать, например, документы WML для использования в браузерах WAP или на самом деле что-то еще с помощью типа MIME.

Технология ASP.NET аналогична таким технологиям, как PHP, ColdFusion и другим, но между ними имеется одно существенное различие. ASP.NET, как предполагает ее название, была создана с целью полной интеграции в платформу .NET, часть которой включает поддержку C#.

Вполне возможно, что читатель обладает опытом работы с последней технологией компании Microsoft для получения динамической генерации содержимого — ASP. В этом случае, он должен, вероятно, знать, что программирование в этой технологии использует язык сценариев, такой как VBScript или JScript. Это работало, но некоторые вещи были затруднительны для тех программистов, которые привыкли использовать 'правильные' языки программирования, что приводило в результате к потере производительности.

Одним из основных отличий, связанных с использованием более развитых языков программирования, является обеспечение полной серверной объектной модели для использования во время работы. ASP.NET предоставляет доступ ко всем элементам управления на странице, как к объектам в обширном окружении. Также на стороне сервера мы имеем доступ ко всем другим требуемым классам .NET, позволяя интеграцию многих полезных служб. Элементы управления, используемые на странице, функциональны, фактически можно делать все то же, что и с классами форм в Windows, что дает большую гибкость. По этой причине страницы ASP.NET, создающие содержимое HTML, часто называют формами Web.

В этой главе мы проведем более подробное рассмотрение ASP.NET, включая то, как она работает, что мы можем с ней делать и где используется C#.

Введение в ASP.NET

ASP.NET пользуется Информационным сервером Интернета (Internet Information Server, IIS) для доставки содержимого в ответ на запросы HTTP. Страницы ASP.NET находятся в файлах с расширением .aspx, и базовая архитектура выглядит следующим образом:

Во время обработки ASP .NET мы имеем доступ ко всем классам .NET, специальным компонентам, созданным на C# или других языках, базам данных и т.д. Фактически мы обладаем такими же возможностями, как при выполнении приложения C#, т.е. использование C# в ASP.NET заключается в эффективном выполнении приложения C#. Файл ASP.NET может содержать любые элементы из следующего списка:

□ Обработка инструкций для сервера

□ Код на C#, VB.NET, JScript.NET или любом другом языке, который поддерживает платформа .NET сейчас или может поддерживать в будущем

□ Содержимое в любой ферме, подходящей для сгенерированного ресурса, такого как HTML

□ Встроенные серверные элементы правления ASP.NET Поэтому, фактически, можно иметь файл ASP.NET, состоящий просто из

Hello!

без какого-либо дополнительного кода или инструкций вообще. Это приводит просто к созданию возвращаемой страницы HTML (так как HTML является используемым по умолчанию выводом страниц ASP.NET), содержащей именно этот текст.

Как мы увидим позже в этой главе, можно также разделить отдельные части кода по различным файлам, что создаст более логичную структуру.

Управление состоянием в ASP.NET

Одно из ключевых свойств страниц ASP.NET состоит в том, что они не обладают состоянием. По умолчанию никакая информация не сохраняется на сервере между запросами пользователя (хотя существуют методы, которые позволяют при желании это делать). На первый взгляд это кажется немного странным, так как управление состоянием кажется существенным для интерактивных сеансов, удобных для пользователя. Однако ASP.NET предоставляет достаточно удобный способ обхода этой проблемы, чтобы сделать управление сеансом почти прозрачным.

По сути, информация, например, о состоянии элементов управления в форме Web (данные, введенные в текстовые поля, выбор из выпадающих списков и т.д.) хранится в скрытых полях представления состояния (viewstate), которые являются частью страницы, сгенерированной сервером и переданной пользователю. Если в дальнейшем требуется серверная обработка типа пересылки данных формы, то происходит возврат (postback) этой информации на сервер. На сервере эта информация используется для повторного заполнения объектной модели страницы, позволяя нам действовать на ней, как если бы изменения были бы сделаны локально.

Мы скоро увидим это в действии и укажем особенности.

Формы Web ASP.NET

Как упоминалось ранее, большая часть функциональности ASP.NET достигается с помощью форм Web. Скоро мы перейдем к этому вплотную и создадим простую форму Web, чтобы получить начальную точку для исследования этой технологии. Прежде всего, однако, мы должны взглянуть на особенности, имеющие отношение к созданию форм Web. Необходимо отметить, что многие разработчики ASP.NET используют для создания файлов просто текстовый редактор, такой как notepad. Дело облегчается тем, что можно, как отмечалось ранее, объединить весь код в одном файле. Код заключается между тегами <script> с использованием двух атрибутов в открывающем теге <script> следующим образом:

<script language="с#" runat="server">

// Серверный код располагается здесь

</script>

Атрибут runat="server" здесь является критически важным (и мы увидим его неоднократно в этой главе), так как он дает указание IIS выполнить этот код на сервере, а не посылать его клиенту, предоставляя, тем самым, доступ к богатому окружению, рассмотренному ранее. Можно поместить наши функции, обработчики событий и т.д. в серверные блоки сценариев.

Если опустить атрибут runat="server", мы, по сути, предоставим клиентский код, который откажет, если он использует какое-либо кодирование в серверном стиле, которое мы увидим в этой главе. Однако могут возникать ситуации, когда понадобиться предоставить клиентский код (на самом деле ASP.NET сам иногда создает некий код в зависимости от возможностей браузера и используемого кода формы Web). К сожалению, мы не можем использовать здесь C#, так как это будет требовать платформы .NET на стороне клиента, что может не всегда существовать, поэтому JScript является, вероятно, лучшей возможностью (так как он поддерживается на большом множестве клиентских браузеров). Чтобы изменить язык, мы просто изменяем значение атрибута language следующим образом:

<script language="jscript">

// Клиентский код расположен здесь, можно также использовать vbscript.

</script>

В равной степени можно создавать файлы ASP.NET в Visual Studio, что прекрасно для нас подходит, так как мы уже знакомы с этой средой для программирования C#. Однако применяемая по умолчанию настройка проекта для приложений Web в этой среде предоставляет чуть более сложную структуру, чем один файл .aspx. Но это не является для нас проблемой, так как делает вещи более логичными (более подходящими для программиста и менее для разработчика Web). На основе этого в данной главе мы будем пользоваться Visual Studio.NET для программирования ASP.NET.

Рассмотрим пример. Создайте новый проект типа C# Type Web Application, как показано ниже:

По умолчанию VS будет использовать расширения FrontPage для настройки приложения Web в требуемом месте, которое может быть удаленным, если сервер Web находится на другой машине. Но и для этого существует альтернативный (и более быстрый) метод, использование файловой системы через LAN (что является, конечно, невозможным, если удаленный сервер Web находится не в той же LAN, что и сервер разработки). Если первый метод отказывает, то VS будет пробовать другой.

Независимо от используемого метода, VS поддерживает локальный кэш всех файлов проекта, причем в синхронизации с файлами на сервере Web.

Через какое-то время Visual Studio должна создать следующее:

□ Новое решение, PCSWebAppl, содержащее приложение Web на C# с именем PCSWebAppl

□ AssemblyInfo.cs — стандартный код для описания сборки

□ Global.asax — глобальная информация и события приложения (будет показано позже в этой главе)

□ PCSWebAppl.disco — файл, описывающий все службы Web в проекте, дающий возможность динамического обнаружения (подробности в следующей главе)

□ Web.config — конфигурационная информация для приложения (будет показано позже в этой главе)

□ WebForm1.aspx — первая страница ASP NET в приложении Web

Мы покажем все сгенерированные файлы в ходе изложения этой и следующих двух глав, в данный момент нам необходимо сосредоточиться на сердцевине приложения — созданном файле .aspx.

Файлы .aspx можно просматривать двумя способами: в виде модели и в виде кода. Это аналогично тому, что используется для форм Windows, как мы видели раньше в этой книге. Начальное представление в VS является модельным представлением:

Текст, показанный здесь по умолчанию, не является текстом, который мы увидим в приложении, это просто примечание от VS, сообщающее, какой режим компоновки выбран. Здесь используется режим GridLayout, который допускает большую гибкость в управлении позиционированием, но можно сменить его на FlowLayout, если требуется более традиционная схема позиционирования типа HTML. Мы рассмотрим это немного позже.

Если выбрать представление HTML с помощью кнопки внизу окна компоновки, мы увидим код, созданный внутри файла .aspx:

<%@ Page language="#" Codebehind="WebForm1.aspx.cs" AutoEventWireup="false" Inherits="PCSWebAppl.WebForm1" %>

<html>

<head>

<meta name=vs_targetSchema content="Internet Explorer 5.0">

<meta name="GENERATOR" Content="Microsoft Visual Studio 7.0">

<meta name="CODE_LANGUAGE" Content="C#">

</head>

<body MS_POSITIONING="GridLayout">

<form method="post" runat="server">

</form>

</body>

</html>

Здесь элемент <html> заполнен несколькими метаданными, которые нас не касаются, и элементом <form> для размещения кода ASP.NET. Наиболее важной вещью в этом элементе является атрибут runat. Точно так же, как в блоках серверного кода, которые мы видели в начале раздела, он задан как server, и значит, обработка формы будет иметь место на сервере. Если не включить этот атрибут, то никакой серверной обработки выполняться не будет, и форма не будет ничего делать.

Другая интересная вещь в отношении этого кода состоит в теге <@% Page %> в начале файла. Этот тег определяет характеристики страницы, которые важны для нас как разработчиков приложения Web на C#. Прежде всего здесь существует атрибут language, который определяет, что на этой странице будет использоваться C#, как мы видели раньше в блоках <script> (значение по умолчанию для приложения Web является VB.NET, хотя это может быть изменено через конфигурацию IIS). Следующие три атрибута являются необходимыми, так как код, управляющий страницей, был задан VS для размещения в отдельном файле WebForm1.aspx.cs. Этот файл, который мы сейчас рассмотрим, содержит определение класса, используемого в качестве базового для страницы форм Web. (Теперь мы начинаем видеть, как ASP.NET соединяется с насыщенной объектной моделью). В этом файле для создания HTML базовый класс будет использоваться в соединении с кодом.

Отметим, что не все файлы .aspx требуют такой многослойной модели, можно использовать просто класс базовый формы Web в .NET в качестве базового класса для страницы, что используется по умолчанию. В этом случае файл .aspx должен включать весь код C# в блоках <script>, как упоминалось ранее.

Так как мы предоставляем специальный базовый класс для страницы, мы будем иметь специальные события. Чтобы гарантировать, что IIS знает об этом, мы используем атрибут AutoEventWireup, который означает, что обработчик событий Page_Load(), вызываемый при загрузке страницы, связывается автоматически с событием OnPageLoad. Задавая этот атрибут как false, мы должны предоставить, если потребуется, свой собственный код для выполнения этого, что даст нам большую свободу действий.

Теперь посмотрим на "код позади" кода, сгенерированного для этого файла. Чтобы сделать это, щелкнем правой кнопкой мыши на WebForm1.aspx в утилите анализа решения (solution explorer) и выберем View Code. Код WebForm1.aspx.cs должен загрузиться в текстовый редактор. Прежде всего можно видеть объявление пространств имен для приложения Web, за которым следует используемое по умолчанию множество ссылок, требуемое для базового использования:

namespace PCSWebAppl {

using System;

using System.Collections;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Web;

using System.Web.SessionState;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.HtmlCotrols;

Двигаясь дальше, мы видим определение WebForm1 — базового класса, используемого для страницы .aspx. Этот класс наследует из System.Web.UI.Page, базового класса форм Web:

/// <summary>

/// Краткое описание WebForm1

/// </summary>

public class WebForm1 : System.Web.UI.Page {

Остальная часть кода в этой форме выполняет различные инициализационные задачи, а также включает код, требуемый для создания форм Web в VS. Конструктор регистрирует обработчика событий Page_Init(), вызываемого во время активации страницы и используемого VS для кода, связанного с созданием временных добавлений к форме (хранимых в InitializeComponent(), который вызывается этим обработчиком). Существует также обработчик событий Page_Load(), упоминавшийся ранее:

public WebForm1() {

Page.Init += new System.EventHandler(Page_Init)

}

protected void Page_Load(object sender, System.EventArgs e) {

// Поместите здесь код пользователя для инициализации страницы

}

protected void Page_Init(object sender, EventArgs e) {

//

// CODEGEN: Этот вызов требуется для ASP.NET Windows

// Form Designer.

//

InitializeComponent();

}

Сам метод InitializeComponent() содержится в блоке #region, поэтому мы используем схематичное представление в VS, чтобы его скрыть, поскольку он быстро заполнится сгенерированным кодом VS (так же как аналогичный ему метод в коде форм Windows):

#region код, созданный Web Form Designer

/// <summary>

/// Метод, требуемый для поддержки Designer; не изменяйте

/// содержимое этого метода с помощью редактора кода.

/// </summary>

private void InitializeComponent() {

this.Load += new System.EventHandler(this.Page_Load);

}

#endregion

}

}

Так как AutoEventWireup был задан как false, то InitializeComponent() должен зарегистрировать Page_Load() с событием Load.

Строго говоря, этот код больше, чем требуется для простой страницы формы Web ASP.NET, которую мы уже видели (хотя и в качестве тривиального примера). Однако созданная структура приспособлена для целей повторного использования и расширения с помощью технологий C#, не требуя заметного объема накладных расходов, поэтому мы будем ее использовать.

Серверные элементы управления ASP.NET

Созданный нами код делает пока еще очень немногое, поэтому далее нам нужно добавить некоторое содержимое. Можно сделать это в VS, используя построитель форм Web, который поддерживает режим перетаскивания элементов и добавления через код точно так же, как построитель форм Windows.

Существует четыре типа элементов управления, которые можно добавлять к страницам ASP.NET:

□ Элементы управления сервера HTML — элементы управления, которые имитируют элементы HTML, известные разработчикам HTML

□ Элементы управления сервера Web — новое множество элементов управления, некоторые из которых имеют такую же функциональность, как и элементы управления HTML, но с общей схемой имен свойств и т.д., служащей для облегчения разработки (и обеспечения согласованности с аналогичными элементами управления форм Windows); существует также несколько совершенно новых и очень мощных элементов управления, как мы увидим позже

□ Элементы управления проверкой достоверности — множество элементов управления, способных выполнять простую проверку ввода пользователя

□ Заказные и обычные элементы управления пользователя — элементы управления, используемые разработчиком, которые можно определить рядом способов, как будет показано в главе 18

Мы увидим полный список элементов управления сервера Web и проверкой достоверности в следующем разделе, вместе с примечаниями по использованию. Они не будут рассматриваться в этой главе. Эти элементы управления не предоставляют ничего такого, что не делают элементы управления сервера Web, а элементы управления сервера Web дают более насыщенную среду для тех, кто больше привык к программированию, чем к проектированию HTML. (Что должно относиться к большинству аудитории этой книги). Умение использовать элементы управления сервера Web, дает возможность пользоваться элементами управления сервера HTML без всяких проблем.

Давайте добавим пару элементов управления сервера Web в наш проект. Все элементы управления сервера Web и проверкой достоверности даются в следующей форме типа элемента XML:

<asp:X runat="server" attribute="value">Contents</asp:X>

Здесь X является именем элемента управления сервера ASP.NET, attribute="value" является одной или несколькими спецификациями атрибутов, a Contents определяет содержимое элемента управления, если оно существует. Некоторые элементы управления позволяют задавать свойства с помощью атрибутов и содержимого элемента управления, например, Label (используемый для вывода простого текста), где текст можно определить любым образом. Другие элементы управления могут использовать схему вложенности элементов для определения их иерархии, например, Table (определяющий таблицу), который может содержать элементы TableRow, чтобы декларативно определить строки таблицы.

Отметим, что, так как синтаксис элементов управления основывается на XML (хотя они могут использоваться встроенными в код, не являющийся XML, такой как HTML, будет ошибкой отсутствие закрывающих тегов, отсутствие /> для пустых элементов или перекрытие элементов управления.

Наконец, мы снова видим атрибут runat="server" в элементе управления сервера Web. Он важен здесь так же, как и в других местах, и распространенной ошибкой является пропуск этого атрибута, приводящий к неработающим формам Web.

В первом примере мы делаем все простым способом. Измените представление в виде кода HTML для WebForm1.aspx так:

<%@ Page language="c#" Codebehind="WebForm1.aspx.cs" AutoEventWireup="false" Inherits="PCSWebAppl.WebForm1" %>

<html>

<head>

<meta name=vs_targetSchema content="Internet Explorer 5.0" >

<meta name="GENERATOR" Content="Microsoft Visual Studio 7.0">

<meta name="CODE_LANGUAGE" Content="C#">

</head>

<body MS_POSITIONING="GridLayout">

<form method="post" runat="server">

<asp:Label Runat="server" ID="resultLabel"/>

<br>

<asp:Button Runat="server" ID="triggerButton" Text="Click Me" />

</form>

</body>

</html>

Обратите внимание, что при вводе этого кода VS пытается предсказать ввод так же, как это делается при создании кода C#.

Здесь мы добавили два элемента управления формы Web — метку и кнопку.

Возвращаясь к окну построения, можно увидеть, что были добавлены эти элементы управления, именованные с помощью своих атрибутов ID. Как и для форм Windows, мы имеем полный доступ к свойствам, событиям и т.д. через окно Properties, и можем видеть немедленные изменения в коде или визуальном представлении, когда производятся эти изменения.

Теперь посмотрим еще раз на WebForm1.aspx.cs. Мы видим, что к классу WebForm1 были добавлены следующие два члена:

protected System.Web.UI.WebControls.Button triggerButton;

protected System.Web.UI.WebControls.Label resultLabel;

Любые добавляемые серверные элементы управления автоматически будут становиться частью объектной модели создаваемой формы.

Чтобы приложение действительно что-то делало, давайте добавим обработчик событий для нажатия кнопки. Здесь мы можем либо ввести имя метода в окне Properties для кнопки, либо просто сделать двойной щелчок на кнопке, чтобы получить используемый по умолчанию обработчик событий. Если сделать двойной щелчок на кнопке, то автоматически добавится следующий метод обработки события:

protectеd void triggerButton_Click(object sender, System EventArgs e) {

}

Он соединяется с кнопкой с помощью кода, добавляемого в InitializeComponent():

private void InitilizeComponent()

this.triggerButton.Click += new System.EventHandler(this.triggerButton_Click);

this.Load += new System.EventHandler(this.Page_Load);

}

Изменим код в triggerButton_Click() следующим образом:

protected void triggerButtor_Click(object sender, System.EventArgs e) {

resultLabel.Text = "Button clicked:";

}

Теперь мы готовы к запуску приложения. Строим приложение в VS обычным образом, все файлы будут откомпилированы и/или помещены на сервер Web готовыми к использованию. Чтобы протестировать приложение Web, можно либо запустить приложение (что предоставит весь набор средств отладки VS), либо просто направить браузер на адрес http://localhost/PCSWebAppl/webForm1.aspx. В любом случае можно будет увидеть кнопку Click Me на странице Web. До нажатия кнопки посмотрим на код, полученный браузером, с помощью View|Source (в IE). Раздел <form> должен выглядеть примерно следующим образом:

<form name="ctrl1" method="post" action="webform1.aspx" id="ctrl1">

<input type="hidden" name="_VIEWSTATE"

value="dDwtMzQ3NzI5OTM4Ozs+0RD39htoKLKMO7Yf41cFXM2GjQU=" />

<span id="resultLabel"></span>

<br>

<input type= "submit" name="triggerButton" value="Click Me" id="triggerButton" />

</form>

Элементы управления сервера Web сгенерировали правильный код HTML — <span> и <input> для <asp:Label> и <asp:Button>, соответственно. Также существует поле <input type="hidden"> с именем _VIEWSTATE. Оно инкапсулирует состояние формы, как это упоминалось ранее. Эта информация используется, когда форма посылается назад на сервер для воссоздания UI, отслеживания изменений и т.д. Отметим, что для этого был сконфигурирован элемент <form>, он будет отправлять данные назад в WebForm1.aspx (определенное в action) с помощью операции HTTP POST (определенной в method). Ему было также присвоено имя ctrl1. Если посмотреть на HTML, сгенерированный более сложными формами Web, то можно увидеть, что это обычный тип присваивания и он соответствует способу, которым работает ASP.NET.

После нажатия на кнопку и появления текста проверьте снова исходный код HTML (пробелы добавлены для ясности)

<form name="ctrl1" method="post" асtion="WebForm1.aspx" id="ctrl1">

<input type="hidden" name="_VIEWSTATE"

value="dDwtMzQ3NzI5OTM4O3Q802w8MTwxPjs+ O2wbdDw7bDwxPDE+Oz47bDx0 PHA8cDxsPFR1eHQ7PjtsPEJ1dHRvbiBjbGlj a2VkITs+Pjs+Ozs+Oz4+Oz4+Oz6TChBE9Yvrgb7dL38o2VsGzc/RgA==" />

<span id="resultLabel">Button clicked</span>

<br>

<input type="submit" name="triggerButton" value="Click Me" id="triggerButton" />

</form>

В этот раз значение VIEWSTATE содержит больше информации, так как результат HTML опирается не только на используемый по умолчанию вывод страницы ASP NET. В сложных формах это может быть на самом деле очень длинная строка, но мы не должны выражать недовольства, так как очень много было сделано для нас "за сценой".

Палитра элементов управления

В этом разделе мы кратко рассмотрим доступные элементы управления, прежде чем соберем их вместе в большом и более интересном приложении. Этот раздел поделен на элементы управления сервера Web и элементы управления проверкой достоверности. Обратите внимание, что в описаниях элементов упрaвлeния ссылка идет на "свойства", во всех случаях соответствующий атрибут для использования в коде ASP.NET называется идентично. Здесь представлены только наиболее часто используемые свойства.

Элементы управления сервера Web

Все элементы управления сервера Web наследуются из класса System.Web.UI.WebControls.WebControl, который, в свою очередь, наследуется из класса System.Web.UI.Control. В связи с этим они обладают многими общими свойствами и событиями, которые при необходимости можно использовать. Их достаточно много, поэтому не все они будут здесь показано, также как и свойства, и события самих элементов управления сервера Web.

Многие из часто используемых унаследованных свойств имеют дело со стилем вывода изображения с помощью таких свойств, как ForeColor, Backcolor, Font и т. д. Но можно прибегнуть также к помощи классов CSS (каскадных таблиц стилей), задавая для строкового свойства CssClass имя класса CSS в отдельном файле. Другими примечательными свойствами являются Width и Height для размера элемента управления, AccessKey и TabIndex для облегчения взаимодействия пользователя, и Enabled для определения того, что функциональность элемента управления обеспечивается в форме Web.

Из событий мы, наверно, чаще всего будем использовать унаследованное событие Event для выполнения инициализации элемента управления, и PreRender для выполнения последних модификаций перед тем, как HTML выведет элемент управления.

Существует множество других событий и свойств, многие из них мы обсудим более подробно позже, при рассмотрении специальных элементов управления. Список элементов управления сервера Web включает в себя:

Элемент управления

Описание

Label

Простой вывод текста, использует свойство Text для задания и программного изменения изображаемого текста.

TextBox

Предоставляет текстовое окно, которое пользователи могут редактировать. Использует свойство Text для доступа к введенным данным и событие TextChanged для действия на изменениях при обратной отправке. Если требуется автоматическая обратная отправка (в противоположность использованию кнопки и т.д.), задайте свойство AutoPostBack как true.

DropDownList

Позволяет пользователю выбрать один вариант из списка выбора либо непосредственно из списка, либо вводя первую букву или две. Использует свойство Items для задания списка позиций (это класс ListItemCollection, содержащий объекты ListItem) и свойства SelectedItem и SelectedIndex для определения того, что выбрано. Событие SelectedIndexChanged может использоваться для выяснения, изменился ли выбор, и этот элемент управления имеет также свойство AutoPostBack, чтобы это изменение выбора включало операцию обратной пересылки.

ListBox

Позволяет пользователю выбрать один или несколько элементов из списка. Задайте SelectionMode как Multiple или Single, чтобы определить, сколько элементов можно выбрать одновременно, и Rows, чтобы определить, сколько элементов показывать. Другие свойства и события такие же, как и у DropDownList.

Image

Выводит изображение. Используйте ImageUrl для ссылки на изображение, и AlternateText для вывода текста, если изображение не может загрузиться.

AdRotator

Выводит несколько изображений по очереди с выводом различных изображений после каждого обращения к серверу. Используйте свойство AdvertisementFile для определения файла XML, описывающего возможные изображения (подробности можно найти в MSDN) и событие ADCreated для выполнения обработки, прежде чем каждое изображение посылается назад. Можно также использовать свойство Target для указания открываемого окна, когда происходит щелчок мышью на изображении.

CheckBox

Выводит флажок, который может быть установлен или не установлен. Состояние хранится в логическом свойстве Checked, а текст, связанный с полем флажка — в свойстве Text. Свойство AutoPostBack может использоваться для инициирования автоматической обратной отправки, а событие CheckedChanged— для действия при изменениях.

CheckBoxList

Создает группу полей флажков. Свойства и события идентичны другим элементам управления списков, таким как DropDownList.

RadioButton

Выводит кнопку, которая может быть включена или выключена. Обычно они группируются, так что только одна кнопка в группе может быть активной, используйте свойство GroupName для соединения элементов управления RadioButton в группу. Другие свойства и события, как в элементе управления CheckBox.

RadioButtonList

Создает группу переключателей, где только одна кнопка в группе может быть выбрана в данный момент времени. Свойства и события — как в других элементах управления списками.

Calendar

Позволяет пользователю выбрать дату на графическом изображении календаря. Этот элемент управления имеет множество свойств, имеющих отношение к стилю, но основная функциональность может быть получена с помощью свойств SelectedDate и VisibleDate (типа System.DateTime), чтобы получить доступ к дате, выбранной пользователем и месяцу для вывода (который всегда будет содержать VisibleDate). Ключевым событием для привязки является SelectionChanged. Обратная отправка из этого элемента управления выполняется автоматически.

Button

Стандартная кнопка для нажатия пользователем. Использует свойство Text для текста и событие Click для ответа на нажатие (обратная отправка на сервер выполняется автоматически). Может также использовать событие Command для ответа на последовательные нажатия, что дает при получении доступ к дополнительным свойствам CommandName и CommandArgument.

LinkButton

Идентичен Button, но выводит кнопку как гиперссылку.

ImageButton

Выводит изображение, которое служит в качестве кнопки для нажатия. Свойства и события наследуются из Button и Image.

HyperLink

Гиперссылка HTML. Задает место назначения с помощью NavigateUrl и текст для вывода с помощью свойства Text. Может также использовать ImageUrl в качестве ссылки для определения изображения для вывода и Target для определения используемого окна браузера. Этот элемент управления не имеет нестандартных событий, поэтому используйте вместо него LinkButton, если потребовалась дополнительная обработка при следовании по ссылке.

Table

Определяет таблицу. Во время проектирования применяйте его в соединении с TableRow и TableCell или программным путем присваивайте строки с помощью свойства Rows, типа TableRowCollection. Это свойство можно также использовать для изменений во время выполнения. Этот элемент управления имеет несколько свойств для стилей, специфических для таблиц, таких же, как в TableRow и TableCell.

TableRow

Определяет строку внутри Table. Ключевым свойством является Cells, которое является классом TableCellCollection, содержащим объекты TableCell.

TableCell

Определяет отдельную ячейку внутри TableRow. Используйте свойство Text для задания текста для вывода, Wrap — для определения, нужно ли сворачивать текст, и RowSpan и ColumnSpan для определения, какую часть таблицы занимает ячейка.

Panel

Контейнер для других элементов управления. Можно использовать HorizontalAlign и Wrap для определения того, как организуется содержимое.

Repeater

Используется для вывода данных из запроса данных, предоставляя большую гибкость с помощью шаблонов. Мы подробно рассмотрим этот элемент управления позже в этой главе.

DataList

Аналогичен элементу управления Repeater, но имеет больше гибкости, когда необходимо организовать и отформатировать данные. Может, например, автоматически вывести таблицу, которую можно будет редактировать. Его мы также будем рассматривать позднее.

DataGrid

Аналогичен Repeater и DataList с несколькими дополнительными возможностями, такими, как сортировка. Подробнее будет рассмотрен позже.

Элементы управления проверкой достоверности

Элементы управления проверкой достоверности предоставляют метод проверки достоверности ввода пользователя (в большинстве случаев) вообще без написания какого-либо кода. Когда инициируется обратная отправка, каждый элемент управления выполняет проверку, которую он подтверждает, и изменяет соответственно свое свойство isValid. Если это свойство будет false, то ввод пользователя для элемента проверки достоверности не получил подтверждение. Страница, содержащая все элементы управления, также имеет свойство isValid: если у какого-либо из элементов управления проверкой достоверности свойство isValid задано как false, то это свойство страницы также будет иметь значение false. Это свойство можно проверять из серверного кода и действовать в соответствии с ним.

Однако элементы управления проверкой достоверности имеют вторую функцию. Они не только проверяют элементы управления во время выполнения, но могут также автоматически выводить пользователям полезные рекомендации. Если задать для свойства ErrorMessage какое угодно текстовое значение, то пользователь увидит его, когда попытается отправить назад неверные данные.

Хранящийся в ErrorMessage текст можно вывести в том месте, где расположен элемент управления проверкой достоверности или в другом месте с сообщениями обо всех других элементах управления проверкой достоверности на странице. Такое поведение достигается с помощью элемента управления ValidationSummary, который выводит, если потребуется, все сообщения об ошибках вместе с дополнительным текстом.

В браузерах, которые это поддерживают, данные элементы управления создают клиентские функции JavaScript, чтобы упростить свое поведение по проверке. Это означает, что в некоторых случат обратная пересылка даже не происходит, так как элементы управления проверкой могут в некоторых ситуациях воспрепятствовать этому и вывести сообщения об ошибках без участия сервера.

Все элементы управления проверкой наследуют из класса BaseValidator, и поэтому обладают некоторыми общими важными свойствами. Возможно, наиболее важным является рассмотренное выше свойство ErrorMessage и в этом случае свойство ControlToValidate можно считать вторым по важности. Это свойство определяет идентификатор (ID) элемента управления, который проверяется. Другим важным свойством является Display, которое определяет, поместить ли текстовое сообщение в итоговой позиции проверки (если задано как none) или в позиции проверяющего элемента. Имеется также возможность оставить место для сообщения об ошибке, даже когда оно не выводится (задавая Display как Static) или динамически выделять место, когда потребуется, что может слегка сдвигать содержимое страницы (задавая Display как Dynamic).

Мы скоро рассмотрим пример, но сначала кратко опишем различные элементы управления проверкой:

Элемент управления

Описание

RequiredFieldValidator

Используется для проверки, ввел ли пользователь данные в элемент управления, такой как TextBox.

CompareValidator

Используется для проверки того, что введенные данные удовлетворяют простым требованиям, происходит сравнение с оператором set, использующим свойство Operator и свойство ValueToCompare. Operator может быть одним из Equal, GreaterThan, GraterThenEqual, LessThen, LessThenEqual, NotEqual или DataTypeCheck. Последний из них просто сравнивает тип данных ValueToCompare с данными в проверяемом элементе управления (ValueToCompare является строковым свойством, но интерпретируется как другой тип данных на основе своего содержимого).

RangeValidator

Проверяет, что данные в элементе управления для проверки находятся между значениями свойств MaximumValue и MinimumValue.

RegularExpressionValidator

Проверяет содержимое поля на основе регулярного выражения, хранящегося в ValidationExpression. Это может быть полезно для известной последовательности, такой как zip-коды, телефонные номера, IP-номера и т.д.

CustomValidator

Применяется для проверки данных в элементе управления с помощью специальной функции. ClientValidationFunction используется для определения клиентской функции, используемой для проверки элемента управления (это означает, к сожалению, что мы не можем использовать C#). Эта функция должна возвращать логическое значение, указывающее, была проверка успешной или нет. Альтернативно можно взять событие ServerValidate для определения серверной функции, используемой для проверки. Эта функция является обработчиком событий с булевым типом, которая получает строку, содержащую данные для проверки вместо параметра EventArgs. Мы возвращаем true, если проверка проходит успешно, иначе false.

Пример серверного элемента управления

Теперь мы разберем простой пример — пришло время взглянуть на более сложный сценарий. Давайте создадим каркас приложения Web — утилиту для заказа помещения для собраний. В данный момент он будет содержать только внешний интерфейс и простую обработку событий, позже мы расширим его с помощью ADO.NET и связывания данных, чтобы включить серверную бизнес-логику.

Форма Web, которую мы собираемся создать, будет содержать поля для имени пользователя, имени события, помещения для собраний и служителей вместе с календарем для выбора даты (предполагается в этом примере, что события продолжаются в течение полного дня). Используем элементы управления проверкой для всех полей, за исключением календаря, который будет проверяться на сервере и предоставлять дату по умолчанию в случае, если ничего не было введено.

Для тестирования интерфейса пользователя мы будем также иметь на форме элемент управления Label, который можно использовать для вывода представляемых результатов.

Вначале создадим в Visual Studio новый проект приложения Web с именем PCSWebApp2. Затем мы создаем форму, которая генерируется с помощью следующего кода в WebForm1.aspx (код, генерируемый автоматически, не выделен):

<%@ Page language="c#" Codebehind="WebForm1.aspx.cs" AutoEventWireup="false" Inherits="PCSWebApp2.WebForm1" %>

<html>

<head>

<meta content=False name=vs_showGrid>

<meta content="Internet Explorer 5.0" name="vs_targetSchema>

<meta content="Microsoft Visual Studio 7.0" name=GENERATOR>

<meta content=C# name=CODE_LANGUAGE>

</head>

<body>

<form method="post" runat="server">

<h1 align="center">Enter details and set a day to initiate an event.</h1>

<br>

<table borderColor="#000000" cellSpacing="0" cellPadding="8"

rules="none" align="center" bgColor="#fff99e" border="2" width="540">

<tr>

<td vAlign="top">Your Name:</td>

<td vAlign="top">

<asp:textbox id="nameBox" runat="server" width="160px" />

<asp:requiredfieldvalidator id=validateName Runat="server"

errormessage="You must enter a name." ControlToValidate="nameBox" display="None" />

</td>

<td vAlign="center" rowSpan="4" >

<asp:calendar id="calendar" runat="server" BackColor="White" />

</td>

</tr>

<tr>

<td vAlign="top">Event Name:</td>

<td vAlign="top">

<asp:textbox id="eventBox" runat="server" width="160px" />

<asp:requiredfieldvalidator id="validateEvent" Runat="server"

errormessage="You must enter an event name."

ControlToValidate="eventBox" display="None" />

</td>

</tr>

<tr>

<td vAlign="top">Meeting Room:</td>

<td vAlign="top">

<asp:dropdownlistid="roomList" runat="server" width="160px">

<asp:ListItem Value="1">The Happy Room</asp:ListItem>

<asp:ListItem Value="2">The Angry Room</asp:ListItem>

<asp:ListItem Value="3">The Depressing Room</aspListItem>

<asp:ListItem Value="4">The Funked Out Room</asp:ListItem>

</asp:dropdownlist>

<asp:requiredfieldvalidator id="validateRoom" Runat="server"

errormessage="You must select a room."

ControlToValidate="roomList" display="None" />

</td>

</tr>

<tr>

<td vAlign= " top">Attendees: </td>

<td vAlign="top">

<asp:listbox id="attendeeList" runat="server" width="60px"

selectionmode="Multiple" rows="6">

<asp:ListItem Value="1">Bill Gates</asp:ListItem>

<asp:ListItem Value="2">Monika Lewinsky</asp:ListItem>

<asp:ListItem Value="3">Vincent Price</asp:ListItem>

<asp:ListItem Value="4">Vlad the Impaler</asp:ListItem>

<asp:ListItem Value="5">Iggy Pop</asp:ListItem>

<asp:Listltem Value="6">William Shakespeare</asp:ListItem>

</asp:listbox>

<asp:requiredfieldvalidator id="validateAttendees" Runat="server"

errormessage="You must have at least one attendee."

ControlToValidate="attendeeList" display="None" />

</td>

</tr>

<tr>

<td align="middle" colSpan="3">

<asp:button id="submitButton" runat="server" width="100%"

Text="Submit meeting room request" />

</td>

</tr>

<tr>

<td align="middle" colSpan="3">

<asp:validationsummary id="validationSummary" Runat="server"

headertext="Before submitting your request:" />

</td>

</tr>

</table>

<br> Results:

<asp:Label Runat="server" ID="resultLabel" Text="None." />

</form>

</body>

</html>

После заголовка страницы, который записан между тегами HTML <h1>, чтобы сделать его крупным текстом в стиле заголовка, основное тело формы помещается между тегами HTML <table>. Мы могли бы использовать управляющий элемент таблицы сервера Web, но это внесло бы ненужную сложность, так как таблица используется только для форматирования вывода, а не как динамический элемент интерфейса пользователя. Таблица делится на три столбца, первый из которых содержит простые текстовые метки, второй содержит поля интерфейса пользователя, соответствующие текстовым меткам (вместе с элементами управления проверкой для них), и третий, содержащий элемент управления календарем для выбора даты, которая размещается на четырех строках. Пятая строка содержит кнопку отправки, охватывающую все столбцы, и шестая строка содержит элемент управления validationSummary для вывода сообщений об ошибках, когда потребуется (все остальные элементы управления проверкой имеют атрибут display="none", так как они будут использовать для вывода это итоговое поле). Под таблицей находится простая метка, которую можно использовать в настоящее время для вывода результатов, пока не будет добавлен доступ к базе данных.

Большая часть кода ASP.NET в этом файле удивительно проста. Специального замечания требует способ, которым элементы списка присоединяются к элементам управления для выбора помещения для встречи, а также нескольких лекторов:

<asp:dropdownlist id="roomList" runat="server" width="160px">

<asp:ListItem Value="1">The Happy Room</asp:ListItem>

<asp:ListItem Value="2">The Angry Room</asp:ListItem>

<asp:ListItem Value="3">The Depressing Room</asp:ListItem>

<asp:ListItem Value="4">The Funked Out Room</asp:ListItem>

</asp:dropdownlist>

...

<asp:listbox id="attendeeList" runat="server" width="160px"

selectionmode="Multiple" rows="6">

<asp:ListItem Value="1">Bill Gates</asp:ListItem>

<asp:ListItem Value="2">Monika Lewinsky</asp:ListItem>

<asp:ListItem Value="3">Vincent Price</asp:ListItem>

<asp:ListItem Value="4">Vlad the Impaler</asp:ListItem>

<asp:ListItem Value="5">Iggy Pop</asp:ListItem>

<asp:ListItem Value="6">William Shakespeare</asp:ListItem>

</asp:listbox>

Здесь объекты ListItem соединяются с двумя элементами управления сервера Web. Эти объекты не являются полноценными элементами управления сервера Web, в связи с чем нам не нужно использовать на них runat="server". Когда страница обрабатывается, то записи <asp:ListItem> используются для создания объектов ListItem, которые добавляются к коллекции Items родительского элемента управления списком. Это облегчает нам инициализацию списков, так как не требуется писать код для этого самостоятельно (мы должны были бы создать объект ListItemCollection, добавить объекты ListItem и затем передать коллекцию элементу управления списком). Конечно, мы можем по-прежнему сделать все это программным путем, если понадобиться.

Созданная форма выглядит следующим образом:

Это полностью функциональный интерфейс пользователя, который поддерживает свое собственное состояние между запросами сервера и проверяет ввод пользователей. Учитывая краткость приведенного выше кода, это больше чем достаточно. Фактически, нам остается сделать еще совсем немного, по крайней мере для этого примера. Необходимо только осуществить событие нажатия кнопки для кнопки отправки на сервер.

В действительности это не совсем так. До сих пор мы не имеем никакой проверки для элемента управления календарем. Тем не менее, это просто, так как невозможно очистить выбор в этом элементе управления, и все, что мы должны сделать, это задать начальное значение. Это возможно в обработчике событий Page_Load() для создаваемой страницы:

private void Page_Load(object sender, System.EventArgs e) {

if (!this.IsPostBack) {

calendar.SelectedDate = System.DateTime.Now;

}

}

В данном случае мы просто выбираем сегодняшнюю дату в качестве начальной точки. Отметим, что мы сначала проверяем, что Page_Load() вызывается в результате операции обратной отправки, проверяя свойство страницы IsPostBack. Если выполняется обратная отправка, то это свойство будет true, и мы оставляем только выбранную дату (в конце концов, мы не хотим потерять выбор пользователя)

Чтобы добавить обработчик нажатия кнопки, надо дважды щелкнуть мышью на кнопке и добавить следующий код:

protected void submitButton_Click(object sender, System.EventArgs e) {

if (this.IsValid) {

resultLabel.Text = roomList.SelectedItem.Text + "has been booked on " +

calendar.SelectedDate.ToLongDateString() + " by " + nameBox.Text +

" for " + eventBox.Text + " event. ";

foreach (ListItem attendee in attendeeList.Items) {

if (attendee.Selected) {

resultLabel.Text += attendee.Text + " , ";

}

}

resultLabel.Text += " and " + nameBox.Text + "will be attending.";

}

}

Здесь мы задаем для свойства Text элемента управления resultLabel результирующую строку, которая появится под основной таблицей. В IE результат после отправки может выглядеть следующим образом:

Если встретятся ошибки, то вместо этого будет активизировано ValidationSummary:

Включение обратной отправки без проверки

Если поэкспериментировать с этим примером какое-то время, то можно заметить, что итоговая проверка появляется в том случае, если изменить дату до ввода каких-либо других данных. Это пример некоторого свойства, которое скорее всего будет раздражать пользователя, который сразу подумает: "Я ввел бы эти данные, если бы мне дали возможность это сделать!". Чтобы обойти этот момент, можно отключить итоговую проверку (используя свойство Enabled), если не нажата кнопка отправки. Однако это ведет к другой проблеме. Элемента управления проверкой способны предотвратить обратную отправку, например, когда нажата кнопка отправки, и динамически заполнить итоговую проверку на клиенте без обращения к серверу. Если отключить итоговую проверку в Page_Load() и включить ее в обработчике события нажатия кнопки, то итоговая проверка никогда не будет выводиться в браузерах, которые поддерживают клиентскую проверку (таких, как IE), так как только включение итоговой проверки будет перехватывать запросы обратной отсылки.

Тогда мы должны отключить все элементы управления проверкой по умолчанию, затем включить заново эти элементы управления в обработчике нажатия кнопки и заставить их проверять, прежде чем мы проверим свойство формы IsValid.

Для выполнения всего этого требуется следующее изменение кода. В Page_Load() мы отключаем все элементы управления проверкой. Мы можем использовать для этого коллекцию формы Validators, она содержит все элемента проверки на странице, поэтому мы можем их все перебрать.

private void Page_Load(object sender, System.EventArgs e)

validationSummary.Enabled = false;

foreach (System.Web.UI.WebControls.WebControl validator in this.Validators) {

validator.Enabled = false;

}

…

}

В методе submitButton_Click() мы сразу включаем все элементы управления проверкой, за исключением итогового, что заставляет их проверять свои соответствующие элементы управления, вызывая метод формы isValid(), и затем проверять, как и раньше, свойство IsValid. Мы добавляем также в эту проверку предложение else, которое снова включает итоговую проверку, если оказывается, что IsValid задан как false. Это предоставляет пользователю обратную связь тогда и только тогда, когда элементы управления имеют недопустимые данные и нажата кнопка отправки.

protected void submitButton_Click(object sender, System.EventArgs e) {

foreach (System.Web.UI.WebControls.WebControl validator in this.Validators) {

validator.Enabled = true;

}

this.Validated);

if (this.IsValid) {

…

} else {

validationSummary.Enabled = true;

}

}

В результате этого эффективно отключается проверка на клиентской стороне, что стоит затраченных усилий, так как предоставляет дополнительное удобство использования.

ADO.NET и связывание данных

Созданное в предыдущем разделе приложение формы Web является полностью функциональным, но содержит только статические данные. Кроме того, событие процесса заказа не содержит устойчивых данных события. Чтобы решить обе эти проблемы, можно воспользоваться ADO.NET и получить доступ к данным, хранящимся в базе данных, так чтобы можно было сохранять и извлекать данные события вместе со списками помещений и служителей.

Соединение данных делает процесс извлечения данных еще легче. Элементы управления, такие как поля списков (и некоторые из более специальных элементов управления) готовы к использованию этой техники. Они могут быть связаны с любым объектом, который предоставляет интерфейс IEnumerable, ICollection или IListSource, что включает объекты DataTable.

В этом разделе мы начнем с модернизации нашего приложения события заказа помещения, а затем пойдем дальше и рассмотрим некоторые другие вещи, которые можно делать со связыванием данных, используя другие элементы управления Web.

Модернизация приложения заказа помещения

Создадим новое приложение Web с именем PCSWebApp3 и скопируем код из созданного ранее приложения PCSWebApp2. Прежде чем начать новый код, давайте рассмотрим базу данных, к которой мы будем обращаться.

База данных

Для этого примера используем базу данных Microsoft Access с именем PCSWebApp3.mdb, которую можно найти вместе с загружаемым кодом для этой книги. Для учета масштаба предприятия имеет смысл использовать базу данных SQL Server, но хотя используемая техника практически одинакова, но Access все же облегчает процесс тестирования. В ходе изложения будут показаны необходимые различия в коде, когда они возникнут. Представленная база данных содержит три таблицы:

□ Attendees содержит список возможных почетных гостей событий.

□ Rooms содержит список возможных помещений для событий.

□ Events содержит список заказанных событий.

Attendees

Таблица Attendees содержит следующие столбцы:

Столбец

Тип

Примечания

ID

AutoNumber, первичный ключ

Идентификационный номер почетного гостя

Name

Text, необходимое значение, 50 символов

Имя почетного гостя

Email

Text, необязательное значение, 50 символов

Адрес e-mail почетного гостя

База данных позволяет хранить сведения о 20 почетных гостях, каждый из которых может иметь адрес e-mail. Другое приложение может автоматически посылать письмо почетным гостям после выполнения заказа. Читателям предлагается реализовать такое приложение в качестве упражнения.

Rooms

Таблица Rooms содержит следующие столбцы:

Столбец

Тип

Примечания

ID

AutoNumber, первичный ключ

Идентификационный номер помещения

Room

Text, требуемое значение, 50 символов

Название помещения

Events

Таблица Events содержит следующие столбцы:

Столбец

Тип

Примечания

ID

AutoNumber, первичный ключ

Идентификационный номер события

Name

Text, требуемое значение, 255 символов

Название события

Room

Number, требуемое значение

Идентификатор помещения для события

AttendeeList

Memo, требуемое значение

Список имен почетных гостей

EventData

Date/Time, требуемое значение

Дата события

Несколько событий представлены в загружаемой базе данных.

Соединение с базой данных

Два элемента управления, которые мы хотели бы связать с данными, — attendeeList и roomList. Чтобы сделать это, мы должны задать свойства DataSource этих элементов управления как таблицы, содержащие данные. Код должен загрузить данные в эти таблицы и выполнить соединение. Оба эти элемента управления имеют также свойства DataTextField и DataValueField, которые определяют, какие столбцы использовать для вывода элементов списка и задания свойств value, соответственно. В обоих случаях можно задать эти свойства во время проектирования как Name и ID, что будет использоваться, как только задается свойство DataSource для заполнения элементами списка элемента управления.

Теперь мы можем сделать это в построителе форм Web. Удалите существующие записи из кода ASP.NET для этих элементов управления. Теперь что объявления будут выглядеть следующим образом:

...

<asp:dropdownlist id="roomList" runat="server" width="160px" datatextfield="Room" datavaluefield="ID" / >

...

<asp:listbox id="attendeeList" runat="server" width="160px" selectionmode="Multiple" rows="6" datatextfield="Name" datavaluefield=" " >

Следующая задача состоит в создании соединения с базой данных. Существует несколько способов это сделать. Как мы видели в главе ADO.NET ранее, обычно для создания нового соединения используется окно Server Explorer. Так как мы работаем с Access, то тип провайдера для этого соединения будет Microsoft Jet 4.0 OLE DB Provider. Когда это будет задано в окне сервера, мы сможем перетащить соединение на форму Web, что добавит объект Data.OleDb.OleDbConnection к форме с именем oleDbConnection1:

public class WebForm1: System.Web.UI.Page {

...

protected System.Data.OleDb.OleDbConnection oleDbConnection1;

Для соединения SQL Server будет добавлен объект SqlClient.SqlConnection.

В метод InitializeComponent() также добавится код для задания свойства ConnectionString формы oleDbConnection1, таким образом все будет готово для использования в коде.

Мы хотим выполнить соединение данных в обработчике событий Page_Load(), так что элементы управления будут полностью заполнены, когда мы захотим использовать их в других частях кода. Приступаем к считыванию данных из базы данных, независимо от того, выполняется ли в данный момент операция обратной отправки (даже если элементы управления списком будут сохранять свое содержимое с помощью viewstate), чтобы гарантировать, что мы имеем доступ ко всем данным, которые могут понадобиться, хотя нам и не нужно выполнять само соединение данных при обратной отправке. Это может показаться слегка расточительным, но читатель при желании может в качестве упражнения добавить дополнительную логику к коду для оптимизации такого поведения. Здесь мы сосредоточимся на том, как заставить все работать, не входя в практические детали.

Весь наш код будет помещен между вызовами методов Open() и Close() нашего объекта соединения:

private void Page_Load(object sender, System.EventArgs e) {

validationSummary.Enabled = false;

foreach (System.Web.UI.WebControls.WebControl validator in this.Validators) {

validator.Enabled = false;

}

oleDbConnection1.Open();

if (!this.IsPostBack) {

calendar.SelectedDate = System.DateTime.Now;

}

OleDbConnection1.Close();

}

Мы вскоре увидим, почему задание данных календаря оставлено внутри этого кода проверки обратной отправки.

Для обмена данными нам необходимо использовать несколько объектов хранения данных. Мы можем объявить их на уровне класса, чтобы мы имели к ним доступ из других функций. Нам понадобится объект DataSet для хранения информации базы данных, три объекта OleDb.OleDbDataAdapter для выполнения запросов на множестве данных и объект DataTable хранения событий для последующего доступа. Они объявляются следующим образом:

public class WebForm1 : System.Web.UI.Page {

...

protected System.Data.DataSet ds;

protected System.Data.DataTable eventTable;

protected System.DataOleDb.OleDbDataAdapter daAttendees;

protected System.DataOleDb.OleDbDataAdapter daRooms;

protected System.Data.OleDb.OleDbDataAdapter daEvents;

Существуют версии SQL Server всех объектов OLE DB, и их использование идентично.

Теперь для Page_Load() надо создать объект DataSet:

private void Page_Load(object sender, System.EventArgs e) {

...

oleDbConnection1.Open();

ds = new DataSet();

Затем мы должны присвоить объектам OleDbDataAdapter запросы и соединить их с объектом соединения:

ds = new DataSet();

daAttendees =

new System.Data.OleDb.OleDbDataAdapter("SELECT * FROM Attendees", oleDbConnection1);

daRooms =

new System.Data.OleDb.OleDbDataAdapter("SELECT * FROM Rooms", oleDbConnection1);

daEvents =

new System.Data.OleDb.OleDbDataAdapter("SELECT * FROM Events", oleDbConnection1);

Затем мы выполняем запросы с помощью вызовов Fill():

daEvents =

new System.Data.OleDb.OleDbDataAdapter("SELECT * FROM Events", oleDbConnection1);

daAttendees.Fill(ds, "Attendees");

daRooms.Fill(ds, "Rooms");

daEvents.Fill(ds, "Events");

Теперь переходим непосредственно к самому соединению данных. Как упоминалось ранее, это несложно и включает задание для свойства DataSource элементов управления соединением таблиц, с которыми мы хотим соединиться:

daEvent s.Fill(ds, "Events");

attendeeList.DataSource = ds.Tables["Attendees"];

roomList.DataSource = ds.Tables["Rooms"];

Этот код задает свойства, но само соединение данных не произойдет, пока не будет вызван метод формы DataBind(), что мы сейчас и сделаем. Но прежде чем это сделать, заполним объект DataTable данными таблицы событий:

roomList.DataSource = ds.Tables["Rooms"];

eventTable = ds.Tables["Events"];

Будем соединять данные только в том случае, если нет обратной отправки, иначе происходит просто обновление данных (которые, по предположению, являются статическими в базе данных в течение выполнения запроса заказа события). Соединение данных при обратной отправке будет также стирать выбранные значения в элементах управления roomList и attendeeList. Мы могли бы сделать об этом замечание перед соединением, а затем обновить их, но проще вызвать DataBind() в существующем операторе if (причина, почему этот оператор содержался в области кода, где открыто соединение с данными):

eventTable = ds.Tables["Events"];

if (!this.IsPostBack) {

calendar.SelectedDate = System.DateTime.Now;

this.DataBind();

}

oleDbConnection1.Close();

}

Теперь выполнение приложения приведет к доступности всех данных о помещениях и приглашенных гостях из элементов управления соединением данных.

Модификация элемента управления календарем

Прежде чем обсуждать добавление событий к базе данных, давайте сделаем модификацию в выводе изображения календаря. Было бы неплохо выводить любой день, где была сделана предварительная заявка, другим цветом, и сделать такие дни недоступными для выбора. Это потребует модификации способа, которым даты задаются в календаре, и способа, которым выводятся ячейки дней.

Начнем с выбора даты. Существует три момента, когда нам необходимо проверять даты заявок на мероприятия и изменять соответственно сделанный выбор: когда мы задаем начальную дату в Page_Load(), когда пользователь пытается выбрать дату из календаря, и когда сделана заявка на мероприятие и мы хотим задать новую дату, чтобы помешать пользователю заказать подряд два мероприятия в один день до выбора новой даты. Так как это должно быть общим свойством, можно создать также скрытый метод для выполнения этого вычисления. Этот метод должен принимать пробную дату в качестве параметра и возвращать дату для использования, которая будет либо той же датой, что и пробная, или следующим доступным днем после пробной даты.

Код, который это делает, getFreeDate(), показан ниже:

private System.DateTime getFreeDate(System.DateTime trialDate) {

if (eventTable.Rows.Count > 0) {

System.DateTime=testDate;

bool trialDateOK = false;

while (!trialDateOK) {

trialDateOK = true;

foreach (System.Data.DataRow testRow in eventTable.Rows) {

testDate = (System.DateTime)TestRow["EventDate"];

if (testDate.Date == trialDate.Date) {

trialDateOK = false;

trialDate = trialDate.AddDays(1);

}

}

}

}

return trialDate;

}

Этот простой код использует объект eventTable, который заполняется в Page_Load(), для извлечения данных о мероприятии. Сначала мы проверяем тривиальный случай, где не заказано никаких мероприятий, в этом случае мы можем просто подтвердить пробную дату, возвращая ее. Затем мы просматриваем даты в таблице Event, сравнивая их с пробной датой. Если мы находим совпадение, то добавляем один день к пробной дате и снова выполняем поиск.

Извлечение даты из DataTable удивительно просто:

testDate = (System.DateTime)testRow["EventDate"];

Преобразование данных столбца в System.DateTime работает прекрасно.

Итак, прежде всего используем getFreeDate() в Page_Load(). Это просто означает внесение небольшого изменения в код, который задает свойство календаря SelectedDate:

if (!this.IsPostBack) {

System.DateTime trialDate = System.DateTime.Now;

calendar.SelectedDate = getFreeDate(trialDate);

this.DataBind();

}

Затем нам нужно ответить на выбор даты в календаре. Чтобы сделать это, просто добавим обработчик событий для события календаря SelectionChanged и выполним сравнение даты с датами существующих мероприятий:

protected void calendar_SelectionChanged(object sender, System.EventArgs e) {

System.DateTime trialDate = calendar.SelectedDate;

calendar.SelectedDate = getFreeDate(trialDate);

}

Этот код идентичен коду в Page_Load().

Третий момент, когда необходимо выполнить эту проверку, возникает при нажатии кнопки заказа. Мы вернемся к этому немного позже, так как мы должны внести несколько изменений.

Затем мы хотим изменить цвет ячейки дня календаря, чтобы обозначить существующие мероприятия. Чтобы сделать это, необходимо добавить обработчик событий для события DayRender объекта календаря. Это событие происходит всякий раз при изображении отдельного дня и предоставляет нам доступ к объекту выводимой ячейки и дате этой ячейки с помощью свойств Cell и Date параметра DayRenderEventArgs функции обработчика. Нам нужно просто сравнить дату изображаемой ячейки с датами в объекте eventTable и покрасить ячейку с помощью свойства Cell.BackColor, если существует совпадение:

protected void calendar_DayRender(object sender, System.Web.UI.WebControls.DayRenderEventArgs e) {

if (eventTable.Rows.Count > 0) {

System.DateTime testDate;

fоreach (System.Data.DataRow testRow in eventTable.Rows) {

testDate = (System.DateTime)testRow["EventDate"];

if (testDate.Date == e.Day.Date) {

e.Cell.BackColor = Color.Red;

}

}

}

}

В данном случае используется красный цвет, который дает нам следующее изображение:

Здесь 15, 27, 28, 29 и 30 марта содержат мероприятия, а пользователь выбирает 17 марта. Теперь, после добавления логики выбора даты, невозможно выбрать день, который показан красным цветом, и если делается такая попытка, то вместо этого будет выбрана следующая дата. Например, щелчок на 28 марта в показанном выше календаре приведет к выбору 31 марта.

Запись мероприятий в базу данных

Обработчик событий submitButton_Click() в настоящее время собирает строку из характеристик мероприятия и выводит ее в элементе управления resultLabel. Чтобы добавить мероприятие в базу данных, нужно реформатировать созданную строку в запрос SQL INSERT и выполнить его.

Большая часть следующего кода выглядит знакомо:

protected void submitButton_Click(object sender, System.EventArgs e) {

foreach (System.Web.UI.WebControls.WebControl validator in this.Validators) {

validator.Enabled = true;

}

this.Validate();

if (this.IsValid) {

String attendees = "";

foreach (ListItem attendee in attendeeList.Items) {

if (attendee.Selected) {

attendees += attendee.Text + " (" + attendee.Value + "), ";

}

}

attendees += " and " + nameBox.Text;

String dateString =

calender.SelectedDate.Date.Date.ToShortDateString();

String oleDbCommand = "INSERT INTO Events (Name, Room, " +

"AttendeeList, EventDate) VALUES ('" + eventBox.Text + "', '" +

roomList.SelectedItem.Value + "', '" +

attendees + "', '" + dateString + "')";

После создания строки запроса SQL можно использовать ее для построения объекта OleDb.OleDbCommand:

System.Data.OleDb.OleDbCommand insertCommand =

new System.Data.OleDb.OleDbCommand(oleDbCommand, oleDbConnection1);

После этого снова открываем соединение, которое было закрыто в Page_Load() (это снова, возможно, не самый эффективный способ реализации, но он прекрасно подходит для целей демонстрации) и выполняем запрос:

oleDbConnection1.Open();

int queryResult = insertCommand.ExecuteNonQuery();

Метод ExecuteNonQuery() возвращает целое число, определяющее, сколько строк таблицы были изменены запросом. Если оно равно 1, то это означает, что вставка была успешной. Если это так, то мы помещаем сообщение об успешном выполнении в resultLabel, выполняем новый запрос для повторного заполнения eventTable и множества данных новым списком мероприятий (мы очищаем сначала множество данных, иначе события будут дублироваться) и изменяем выбор в календаре на новую, свободную, дату:

if (queryResult == 1) {

resultLabel.Text = "Event Added.";

daEvents =

new System.Data.OleDb.OleDbDataAdapter("SELECT * FROM Events" oleDbConnection1);

dsClear();

daEvents.Fill(ds, "Events");

eventTable = ds.Tables["Events"];

calendar.SelectedDate =

getFreeDate(calendar.SelectedDate.AddDays(1));

}

Если ExecuteNonQuery() возвращает число, отличное от единицы, то это говорит о том, что возникла проблема. В данном примере мы не будем это затрагивать, а просто выведем уведомление об отказе в resultLabel:

else {

resultLabel.Text = "Event not added due to DB access " + "problem.";

}

И в конце мы снова закрываем соединение:

oleDbConnection1.Close();

} else {

validationSummary.Enabled = true;

}

}

Таким образом, версия с доступом к базе данных приложения для предварительной заявки мероприятий завершена.

Отметим, что в связи с синтаксисом запроса SQL INSERT, мы должны избегать некоторых символов в названии мероприятия, таких как апострофы " ' ", так как они будут вызывать ошибку. Было бы относительно легко реализовать специальное правило проверки, которое не разрешает пользователям применять такие символы или выполнять некоторый тип кодирования символов перед вставкой данных и после считывания данных, но код для выполнения этого не будет здесь рассмотрен.

Еще о связывании данных

Когда мы рассматривали ранее в этой главе доступные серверные элементы управления, мы видели три из них, которые имеют дело с выводом данных: DataGrid, Repeater и DataList. Все они будут очень полезны при выводе данных на страницу Web, так как они автоматически выполняют многие задачи, которые иначе потребовали бы большого объема кодирования.

Прежде всего рассмотрим простейший элемент DataGrid. В качестве примера этого элемента управления давайте добавим вывод данных мероприятия в нижней части окна приложения PCSWebApp3. Это позволяет проигнорировать соединения с базой данных, так как они уже настроены для этого приложения:

Добавим следующие строки в нижней части PCSWebApp3.aspx:

<br>Results:

<asp:label id=resultLabel Runat="server" Text="None.">None.</asp:label>

<br> <br>

<asp:DataGrid Runat="server" ID="eventDetails1" />

</form>

</body>

</HTML>

А следующий код добавим в метод Page_Load() в PCSWebApp3.aspx.cs:

attendeeList.DataSource = ds.Tables["Attendees"];

roomList.DataSource = ds.Tables["Rooms"];

eventTable = ds.Tables["Events"];

eventDetails1.DataSource = eventTable;

if (!this.IsPostBack) {

calendar.SelectedDate = System.DateTime.Now;

this.DataBind();

} else {

eventDetails1.DataBind();

}

oleDbConnection1.Close();

}

Отметим, что список мероприятий может изменяться между запросами в случае, если другой пользователь добавляет мероприятие, поэтому нам необходимо вызывать DataBind() для DataGrid, чтобы отразить эти изменения. Помните, что вызов DataBind() для всей форме приведет к потере выбранного помещения и почетного гостя, это честный компромисс.

Если снова загрузить приложение в браузер Web, то можно увидеть ниже раздела данных заявки полный список мероприятий:

ID

Name

Room

AttendeeList

EventDate

1

My Birthday

4

Iggy Pop (5), Sean Connery (7), Albert Einstein(10), George Clooney (14), Jules Verne (18), Robin Hood (20) and Karli Watson

17.09.2001 00:00:00

2

Dinner

1

Bill Gates (1), Monika Lewinsky (2) and Bruce Lee

05.08.2001 00:00:00

5

Discussion of darkness

6

Vlad the Impaler (4), Darth Vader and Beelzebub

29.10.2001 00:00:00

6

Christmas with Pals

9

Dr Frank N Futer (11), Bobby Davro (15), John F Kennedy (16), Stephen King (19) and Karli Watson

25.12.2001 00:00:00

7

Escape

17

Monika Lewinsky (2), Stephen King (19) and Spartacus

10.05.2001 00:00:00

8

Planetary Conquest

14

Bill Gates (1), Albert Einstein (10), Dr Frank N Furter (11), Bobby Davro (15) and Darth Vader

15.06.2001 00:00:00

9

Homecoming Celebration

7

William Shakespeare (6), Christopher Columbus (12), Robin Hood (20) and Ulysses

22.06.2001 00:00:00

10

Dalek Reunion Ball

12

Roger Moore (8), George Clooney (14), Bobby Davro (15) and Davros

12.06.2001 00:00:00

11

Romantic meal for two

13

George Clooney (14) and Donna Watson

29.03.2001 00:00:00

Мы можем сделать также еще одну модификацию в submitButton_Click(), чтобы гарантировать, что эти данные обновляются, когда добавляются новые записи:

if (queryResult == 1) {

resultLabel.Text = "Event Added.";

daEvents =

new System.Data.OleDb.OleDbDataAdapter("SELECT * FROM Events", oleDbConnection1);

ds.Clear();

daEvents.Fill(ds, "Events");

eventTable = ds.Tables["Events"];

calendar.SelectedDate =

getFreeDate(calendar.SelectedDate.AddDaysd));

eventDetails1.DataBind();

}

Отметим, что мы вызываем DataBind() на DataGrid, а не на this. Это препятствует обновлению любых данных элементов управления соединения, что было бы не нужно. Все элементы управления соединением данных поддерживают этот метод, который обычно вызывается формой, если вызывается метод верхнего уровня (this)dataBind().

Элемент управления DataGrid содержит немало свойств, которые можно использовать для форматирования выводимых данных в более удобном для пользователя виде, но это оставляется читателю для самостоятельного рассмотрения.

Вывод данных с помощью шаблонов

Два других элемента управления выводом данных — Repeater и DataList, требуют использования шаблонов для форматирования данных для вывода. Шаблонами в смысле ASP.NET являются параметризованные разделы кода HTML, которые используются как элементы вывода в некоторых элементах управления. Они позволяют точно определить, как данные выводятся в браузере, и могут создать без существенных усилий профессионально сделанное представление.

Существует несколько шаблонов для настройки различных аспектов поведения списка, но шаблоном, который является важным для Repeater и DataList, является шаблон <ItemTemplate>, используемый при выводе каждого элемента данных. Мы объявляем этот шаблон (и все остальные) внутри объявления элемента управления, например:

<asp:DataList Runat="server" ... >

<ItemTemplate>

...

</ItemTemplate>

</asp:DataList>

Внутри объявлений шаблонов нам нужно будет выводить разделы HTML, включая параметры с данными, связанными с элементом управления. Существует специальный синтаксис, который можно использовать для вывода таких параметров:

<%# expression %>

expression может быть просто выражением, связывающим параметр со свойством страницы или элемента управления, но, скорее, состоит из выражения DataBinder.Eval(). Эта полезная функция может использоваться для вывода данных из таблицы, связанной с элементом управления, определяя столбец с помощью следующего синтаксиса:

<%# DataBinder.Eval(Container.DataItem, "ColumnName") %>

Существует также необязательный третий параметр, позволяющий сформатировать возвращаемые данные, который имеет синтаксис, идентичный выражениям форматирования строк, используемым в других местах.

Полное описание доступных шаблонов дано в следующей таблице:

Шаблон

Описание

<ItemTemplate>

Шаблон используется для элементов списка.

<HeaderTemplate>

Шаблон используется для вывода перед списком.

<FooterTemplate>

Шаблон используется для вывода после списка.

<SeparatorTemplate>

Шаблон используется между элементами списка.

<AlternatingItemTemplate>

Шаблон для альтернативных элементов, способствует проявлению видимости.

<SelectedItemTemplate>

(Только DataList) Шаблон используется для выбранных элементов в списке.

<EditItemTemplate>

(Только DataList) Шаблон используется для элементов в списке, которые редактируются.

Рассмотрим это на примере. Используем для него запрос существующих данных в PCSWebApp3.

Пример

Расширим таблицу вверху страницы, чтобы она содержала DataList, выводящий каждое из мероприятий, хранящихся в базе данных. Мы сделаем эти мероприятия выбираемыми, чтобы данные любого мероприятия можно было вывести, щелкая на его имени. Изменения в коде в PCSWebApp3 показаны ниже:

<tr>

<td align=middle colSpan=3>

<asp:validationsummary id=validationSummary Runat="server" headertext="Before submitting your request:" />

</td>

</tr>

<tr>

<td align-left colSpan=3 width="100%">

<table cellspacing=4>

<tr>

<td width="40%" bgcolor="#ccffcc" >

<asp:DataList Runat="server"

ID="eventDetails2"

OnSelectedIndexChanged="eventDetails2_SelectedIndexChanged" >

<ItemTemplate>

<asp:LinkButton Runat="server" CommandName="Select"

forecolor="#0000ff" ID="Linkbutton1">

<%# DataBinder.Eval(Container.DataItem, "Name") %>

</asp:LinkButton>

<br>

</ItemTemplate>

<SelectedItemTemplate>

<b><%# DataBinder.Eval(Container.DataItem, "Name") %></b>

<br>

</SelectedItemTemplate>

</asp:DataList>

</td>

<td valign="top">

<asp:Label Runat="server" ID="edName"

Font-Name="Arial" Font-Bold="True"

Font-Italic="True" Font-Size="14"> Select an event to view details. </asp:Label>

<br>

<asp:Label Runat="server" ID="edDate" />

<br>

<asp:Label Runat="server" ID="edRoom" />

<br>

<asp:Label Runat="server" ID="edAttendees" />

</td>

</tr>

</table>

</td>

</tr>

</table>

Здесь мы добавили новую строку таблицы, содержащую сведения с DataList в одном столбце и представленные данные в другом. Представление данных является просто четырьмя метками для свойств мероприятия, одна из которых содержит текст "Select an event to view details", когда не выбрано никакого мероприятия (ситуация при первой загрузке формы).

DataList использует <ItemTemplate> и <SelectedItemTemplate> для вывода данных мероприятия. Чтобы облегчить выбор, мы инициируем команду Select внутри <ItemTemplate>, что автоматически изменяет выбор. Мы используем также для заполнения данными мероприятия событие OnSelectedIndexChanged, которое включается, когда команда Select изменяет выбор. Обработчик событий для этого показан ниже (Отметим, что нам нужно сначала выполнить метод DataBind() для обновления выбора):

protected void eventDetails2_SelectedIndexChanged(object sender, System.EventArgs e) {

eventDetails2.DataBind();

DataRow selectedEventRow =

eventTable.Rows[eventDetails2.SelectedIndex];

edName.Text = (string)selectedEventRow["Name"];

edDate.Text =

"<b>Date:</b> " +

((DateTime)selectedEventRow["Event Date"]).ToLongDateString();

edAttendees.Text =

"<b>Attendees:</b> " + (string)selectedEventRow["AttendeeList"];

DataRow selectedEventRoomRow =

ds.Tables["Rooms"].Rows[(int)selectedEventRow["Room"] -1];

edRoom.Text = "<b>Room:</b> " + selectedEventRoomRow["Room"];

}

Здесь данные в ds и eventTable используются для заполнения деталями мероприятия.

Как и в случае DataGrid ранее, необходимо в Page_Load() задать и связать данные для eventDetails2:

eventDetails1.DataSource = eventTable;

eventDetails2.DataSource = eventTable;

...

eventDetails1.DataBind();

eventDetails2.DataBind();

И заново связать в submitButton_Click():

eventDetails1.DataBind();

eventDetails2.DataBind();

Детали мероприятия представлены в таблице:

О шаблонах и элементах управления связыванием данных можно было бы написать целую книгу. Однако изложенного материала в данном разделе вполне достаточно для того, чтобы начать работать.

Конфигурация приложения

В этой главе без каких-либо подробностей упоминалось о существовании концептуального приложения, содержащего страницы и конфигурационные настройки. Это важная концепция для восприятия, особенно при конфигурировании web-сайтов множеством одновременных пользователей.

Сразу сделаем несколько замечаний о терминологии и времени жизни приложения.

Приложение определяется как все файлы проекта, оно сконфигурировано с помощью файлов web.config. Объект Application создается, когда приложение запускается в первый раз, что происходит, когда поступает первый запрос HTTP. В это время также срабатывает событие Application_Start, обработчик событий для которого детально описывается в global.asax (вместе со всеми другими событиями, обсуждаемыми здесь), и создается пул экземпляров HttpApplication. Каждый входящий запрос получает один из этих экземпляров, который выполняет обработку запроса (это означает, что объекты HttpApplication не нуждаются в копировании при одновременном доступе, в отличие от глобального объекта Application). Когда все экземпляры HttpApplication заканчивают свою работу срабатывает событие Application_End, и приложение прекращается, разрушая объект Application.

Когда отдельный пользователь использует приложение Web, запускается сеанс. Как и в случае приложения, это включает создание специфического для пользователя объекта Session вместе с включением события Session_Start. В течение сеанса в отдельные запросы могут входить события Application_BeginRequest и Application_EndRequest. Это повторяется несколько раз за сеанс, когда в приложении происходит доступ к различным ресурсам. Отдельные сеансы могут прекращаться вручную или будут прерываться, если не получают больше никаких запросов. Прекращение сеанса включает событие Session_End и разрушение объекта Session.

Как этот процесс может нам помочь? Существует несколько вещей, которые можно сделать, чтобы рационализировать приложение. Вернемся к приложению, которое разрабатывалось в этой главе. Каждый раз при доступе к странице .aspx множество записей заполняется содержимым PCSWebApp3.mdb. Это множество записей всегда используется только для считывания данных, так как для добавления мероприятий в базу данных используется другой метод. В таких случаях можно заполнить множество записей в обработчике событий Application_Start и сделать его доступным для всех пользователей. Единственный раз, когда понадобиться обновить множество записей, возникнет, если будет добавлено событие. Это существенно повышает производительность, так как в большинстве запросов не будет требоваться доступ к базе данных.

Другая техника, которую можно использовать, состоит в хранении информации уровня сеанса для использования отдельными пользователями между запросами. Это включает извлечение специфической для пользователя информации из хранилища данных при первом соединении пользователя, которая будет доступна, пока пользователь не прекратит посылать запросы или явно выйдет из системы.

Такие технические приемы не будут здесь рассматриваться подробно, так как лучше использовать для этого специальные книги, посвященные ASP.NET, но это поможет, тем не менее, более широкому пониманию процессов. В следующей главе, имеющей дело со службами Web, мы увидим некоторые из этих технологий в действии.

Заключение

В этой главе был представлен обзор создания приложения Web с помощью ASP.NET. Мы видели, как можно использовать информацию о C#, которая была дана в этой книге совместно с элементами управления сервера Web для получения насыщенной среды разработки. Мы разработали приложение для заказа помещения для проведения мероприятий, чтобы проиллюстрировать многие из доступных технологий, таких как существующие различные серверные элементы управления и соединение данных с ADO.NET.

В следующих двух главах мы рассмотрим две более важные темы Web: службы Web и специальные элементы управления. Мы продолжим разработку примера из этой главы, развивая его в различных направлениях, чтобы проиллюстрировать инструменты, имеющиеся в нашем распоряжении.

В заключение можно сказать, что ASP.NET является новым оружием в арсенале разработчика Web: серверная обработка на данный момент является непревзойденной, а мощнейшие возможности C# и платформы .NET очень привлекательны для пользователей.

Глава 17

Службы Web

Службы Web — это новый способ выполнения удаленного вызова методов посредством HTTP с помощью SOAP (Simple Object Access Protocol — простой протокол доступа к объектам). Раньше это было связано с трудностями, что может засвидетельствовать каждый, кто имеет опыт работы с DCOM (Distributed COM — распределенный COM). Создание экземпляра объекта на удаленном сервере, вызов метода и получение результата были далеко не простыми, а необходимая конфигурация была еще более сложной.

SOAP существенно упрощает ситуацию. Эта технология является стандартом на основе XML который определяет, как можно делать вызовы методов через HTTP воспроизводимым образом. Удалённый сервер SOAP способен понимать эти вызовы и выполнять всю трудную работу, такую как создание экземпляра требуемого объекта, выполнение вызова и возврат клиенту ответа, форматированного SOAP.

Как и в случае ASP.NET мы обладаем всеми возможностями технологий C# и .NET на сервере, но более важно, что простое использование служб Web можно получить на любой платформе, имеющей к серверу доступ HTTP. Другими словами, вполне возможно что код Linux мог бы, например, использовать службы .NET.

Кроме того, службы Web можно полностью описать с помощью WSDL (Web Service Description Language — язык описания служб Web), допуская динамический поиск cлужб Web во время выполнения приложения. WSDL предоставляет с помощью XML со схемами XML описания всех методов (вместе с типами данных, требуемыми для их вызова). Существует широкое множество типов данных, доступных для служб Web, которые простираются от простых примитивных до полноценных объектов DataSet, так что базы данных, расположенные полностью в памяти, могут маршализоваться клиенту, что может в результате привести и существенному сокращению нагрузки на сервер базы данных.

Эту главу мы начнем с синтаксиса SOAP и WSDL, а затем перейдем к их использованию службами Web. Мы обсудим, как предоставлять и использовать службы Web, и разберем полный пример, построенный на основе приложения заказа помещения для проведения мероприятий из предыдущей главы.

SOAP

Как упоминалось выше, одним из способов обмена данными со службами Web является SOAP. Эта технология широко обсуждалась в прессе, особенно с тех пор, как компания Microsoft решила принять ее для использования на платформе .NET. Теперь волнение, кажется, слегка успокоилось, так как спецификация SOAP была завершена. Если подумать, то знание того, как точно работает SOAP, похоже на знание того, как работает HTTP, в принципе это интересно, но не существенно. Большую часть времени нам нет необходимости беспокоиться о формате сделанного со службами Web обмена, он просто происходит, а мы получаем требуемый результат, и все довольны.

По этой причине здесь не будет представлено подробное рассмотрение, но будет показано несколько простых запросов и ответов SOAP, чтобы можно было получить некоторое представление о том, что происходит за сценой.

Давайте предположим, что необходимо вызвать метод службы Web, имеющий следующую сигнатуру.

int DoSomething(String stringParam, int intParam)

Далее представлены требуемые для этого заголовки SOAP и body. Вверху указан адрес службы Web (об этом больше будет сказано далее):

POST /SomeLocation/myWebService.asmx HTTP/1.1

Host: karlivaio

Content-Type: text/xml; charset=utf-8

Content-Length: length

SOAPAction: "http://tempuri.org/DoSomething"

<?xml version="1.0"?>

<soap:Envelope xmlns:xsi="http://www.w3.org/2000/10/XMLSchema-instance"

xmlns:xsd="http://www.w3.org/2000/10/XMLSchema"

xmlns:soap="http://schemas.xmlsoap.org/soap/envelope">

<soap:Body>

<DoSomething xmlns="http://tempuri.org/">

<stringParam>string</stringParam>

<intParam>int</intParam>

</DoSomething>

</soap:Body>

</soap:Envelope>

Параметр length определяет здесь общую длину содержимого в байтах и будет меняться в зависимости от значений, посланных в параметрах string и int.

Используемое пространство имен soap определяет различные элементы, которые применяются для создания сообщения. При отправке этого кода через HTTP реальные посылаемые данные будут несколько другими. Например, можно было бы вызвать приведенный выше метод с помощью простого метода GET:

GET /PCSWebSrv1/Service1.asmx/AddEvent?stringParam=string&intParam= int HTTP/1.1 Host.: hostname

Ответ SOAP этого метода будет следующим:

HTTP/1.1 200 OK

Content-Type: text/xml; charset=utf-8

Content-Length: length

<?xml version="1.0" ?>

<soap:Envelope xmlns:xsi="http://www.w3.org/2000/10/XMLSchema-instance"

xmlns:xsd="http://www.w3.org/2000/10/XMLSchema"

xmlns:soap="http://schemas.xmlsoap.org/soap/envelope">

<soap:Body>

<DoSomethingResponse xmlns="http://tempuri.org/">

<DoSomethingResult>int</DoSomethingResult>

</DoSomethingResponse>

</soap:Body>

</soap:Envelope>

где length снова изменяется согласно содержимому, в этом случае int.

И снова реальный ответ через HTTP может быть значительно проще, например:

HTTP/1.1 200 OK

Content-Type: text/xml; charset=utf-8

Content-Length: length

<?xml version="1.0"?>

<int xmlns="http://tempuri.org/">int</int>

Это совсем простой формат XML.

Как упоминалось в начале раздела, все это можно полностью игнорировать. Только при необходимости сделать что-то действительно необычное точный синтаксис становится важным, но такие случаи выходят за рамки этой книги.

WSDL

WSDL полностью описывает службы Web, доступные методы и различные способы вызова этих методов. Все детали этого опять же не так уж важны, но общее понимание будет полезно.

WSDL имеет синтаксис, полностью соответствующий XML, и определяет службы Web по доступным методам, типам данных, используемых этими методами, форматам сообщений запросов и ответов, посылаемых методам и из методов с помощью различных протоколов (чистый SOAP, HTTP GET и т.д.), и различным связываниям между упомянутыми выше элементами.

Возможно, что наиболее важной частью файла WSDL является раздел определения типов данных. Он использует схемы XML для описания формата обмена данными и их отношениями с помощью элементов XML.

Например, метод службы Web, использованной в качестве примера в последнем разделе:

int DoSomething(string stringParam, int intParam)

будет иметь типы, объявленные для запроса следующим образом:

<?xml version="1.0" ?>

<definitions xmlns:s="http://www.w3.org/2000/10/XMLSchema"

xmlns="http://schemas.xmlsoap.org/wsdl/"

... другие пространства имен ... >

<types>

<s:schema attributeFormDefault="qualified" elementFormDefault="qualified"

targetNamespace="http://tempuri.org/">

<s:import namespace="http://www.w3.org/2000/10/XMLSchema" />

<s:element name="DoSomething" >

<s:complexType>

<s:sequence>

<s:element name="stringParam" nullable="true" type="s:string" />

<s:element name="intParam" nullable="true" type="s:int" />

</s:sequence>

</s:complexType>

</s:element>

<s:element name="DoSomethingResponse">

<s:complexType>

<s:sequence>

<s:element name="DoSomethingResult" type="s:int" />

</s:sequence>

</s:complexType>

</s:element>

<s:element name="int" type="s:int" />

</s:schema>

</types>

... другие определения ...

</definitions>

Все, что требуется для запросов и ответов SOAP и HTTP, которые мы видели ранее, определяется этими типами, которые далее в этом файле связываются с такими операциями. Все типы определяются с помощью стандартного синтаксиса схемы XML, например:

<s:element name="DoSomethingResponse">

<s:complexType>

<s:sequence>

<s:element name="DoSomethingResult" type="s:int" />

</s:sequence>

</s:complexType>

</s:element>

Этот код определяет, что элемент с именем <DoSomethingResponse> имеет элемент-потомок с именем <DoSomethingResult>, который содержит целое число.

Если мы имеем доступ к коду WSDL для службы Web, то мы можем его использовать. Как мы скоро увидим, это не так уж трудно сделать.

Теперь, когда мы кратко ознакомились с SOAP и WSDL, пришло время посмотреть, как создаются и используются службы Web.

Службы Web

Обсуждение служб Web включает два вопроса:

□ Создание служб Web, которое связано с написанием служб Web и размещением их на серверах Web.

□ Использование служб Web, которое связано с применением на стороне клиента созданных служб.

Создание служб Web

Службы Web создают, либо помещая код прямо в файлы .asmx, либо, ссылаясь на классы службы Web из этих файлов. Как и со страницами ASP.NET, создание службы Web в VS.NET применяет последний подход, и он также будет использоваться для целей демонстрации.

Создание проекта службы Web, называемой PCSWebSrv1, как показано выше, приводит, как и для проекта приложения Web, к аналогичному множеству созданных файлов. Фактически, единственное различие состоит в том, что вместо создания файла с именем WebForm1.aspx создается файл с именем Service1.asmx. Созданный файл .vsdisco отвечает за идентификацию службы Web, чтобы система Visual Studio .NET, как мы вскоре увидим, могла добавить на него ссылку Web.

Код в Service1.asmx не доступен непосредственно через VS.NET, но просмотр с помощью Notepad показывает следующую строку кода:

<%@ WebService Language="c#" Codebehind="Service1.asmx.cs" Class="PCSWebSrv1.Service1" %>

Этот код ссылается на файл кода, который можно увидеть в VS.NET, — Service1.asmx.cs, доступный при щелчке правой кнопкой мыши на Service1.asmx в Solution Explorer и выборе View Code. Созданный код с удаленными для краткости комментариями показан ниже:

namespace PCSWebSrv1 {

using system;

using System.Collections;

using System.ComponentModel;

using System.Data;

using System.Diagnostics;

using System.Web;

using System.Web.Services;

public class Service1 : System.Web.Services.WebService {

public Service1() {

InitializeComponent();

}

private void InitializeComponent() {

}

public override void Dispose() {

}

}

}

Этот код определяет пространство имен PCSWebSrv1 с несколькими ссылками на стандартные пространства имен и класс службы Web с именем Service1 (ссылку на который мы видели выше в файле Service1.asmx), производный от System.Web.Services.WebService. Мы должны предоставить методы для этого класса службы Web.

Добавление метода, доступного через службу Web, требует простого определения метода как public и задание для него атрибута WebMethod. Этот атрибут помечает методы, которые мы хотим сделать доступными. Вскоре мы рассмотрим типы данных, которые можно использовать для возвращаемого типа и для параметров, но пока добавим следующий метод:

[WebMethod]

public String CanWeFixIt() {

return "Yes we can!";

}

и откомпилируем метод.

Можно проверить, как это будет работать, направляя браузер Web на файл Service1.asmx:

Щелчок на имени метода предоставляет нам информацию о запросе и ответе SOAP, а также примеры того, как запрос и ответ будут выглядеть с помощью методов HTTP GET и HTTP POST. Можно также протестировать метод, нажимая на предоставленную кнопку Invoke (если метод требует простых параметров, их также можно ввести в этой форме). Если сделать это, мы увидим код XML, возвращаемый вызовом метода:

<?xml version="1.0" ?>

<string xmlns="http://tempuri.org/">Yes we can!</string>

Это показывает, что метод работает прекрасно.

Следование по ссылке Service Description, показанной на экране браузера выше, позволяет увидеть описание WSDL службы Web. Наиболее важной частью, имеющей к нам отношение, является описание типов элементов для запросов и ответов:

<types>

<s:schema attributeFormDefault="qualified" elementFormPefault="qualified"

targetNamespace="http://tempuri.org/">

<s:element name="CanWeFixIt">

<s:complexType />

</s:element>

<s:element name="CanWeFixItResponse">

<s:complexType>

<s:sequence>

<s:element name="CanWeFixItResult" nullable="true" type="s:string" />

</s:sequence>

</s:complexType>

</s:element>

<s:element name="string" nullable="true" type="s:string" />

</s:schema>

</types>

Это описание содержит также описание типов данных, требуемых для запросов и ответов, а также различных соединений для службы, делая его достаточно длинным файлом.

Типы данных, доступные для служб Web

Службы Web могут использоваться для обмена любыми из следующих типов данных:

String           Char    Byte

Boolean          Int16   Int32

Int64            UInt16  UInt32

UInt64           Single  Double

Guid             Decimal DateTime

XmlQualifiedName class   struct

XmlNode          DataSet

Массивы всех приведенных выше типов данных также допустимы. Отметим, также, что маршализуются только открытые свойства и поля типов class и struct.

Использование служб Web

Теперь, когда мы знаем, как создавать службы Web, пришло время разобраться, как они используются. Чтобы сделать это, необходимо создать в коде класс прокси, который знает, как общаться с заданной службой Web. Любые обращения из кода к службе Web будут проходить через этот прокси, который выглядит идентично службе Web, создавая в коде иллюзию, что имеется локальная копия службы Web. В реальности существует большой объект коммуникации HTTP, но мы защищены от деталей. Для этого существуют два способа. Можно пользоваться либо утилитой командной строки WSDL.exe, либо пунктом меню Add Web Reference в VS.NET.

При использовании утилиты WSDL.exe создается файл .cs, содержащий класс прокси на основе описания WSDL службы Web. Мы определяем это с помощью URL, например:

WSDL http://localhost/PCSWebSrv1/Service1.asmx?WSDL

Для примера из последнего раздела эта утилита создаст файл с именем Service1.cs класса прокси. Класс называется по имени cлужбы Web, в данном случае Service1, и будет содержать методы, которые вызывают идентично названные методы службы. Чтобы использовать этот класс, мы просто добавляем файл .cs, созданный для проекта, и используем следующий код:

Service1 myService = new Service1();

String result = myService.CanWeFixIt();

По умолчанию созданный класс будет помещен в корневое пространство имен, поэтому не нужен никакой оператор using, но можно определить для использования другое пространство имен с помощью параметра командной строки /n<namespace> утилиты WSDL.exe.

Эта техника хорошо работает, но может оказаться неудобным непрерывно переделывать код, если служба разрабатывается и постоянно изменяется. Конечно, можно было бы делать это в режиме сборки проекта, чтобы автоматически обновлять созданный прокси перед каждой компиляцией, но существует способ лучше.

Мы проиллюстрируем его в новом приложении Web с именем PCSWebClient1, создавая клиента для примера из последнего раздела. В теле формы на созданной странице .aspx необходимо заменить существующее объявление form на следующий код:

<form method="post" runat="server">

<asp:Label Runat="server" ID="resultLabel" />

<br>

<asp:Button Runat="server" ID="triggerButton" Text="Invoke CanWeFixIt()" />

</form>

Соединим обработчик события нажатия кнопки со службой Web. Для начала добавим в проект ссылку на службу Web. Чтобы сделать это, щелкнем правой кнопкой мыши на приложении в Solution Explorer и выберем пункт Add Web Reference… В появившемся окне введем URL файла .vsdisco службы Web:

Здесь можно следовать по ссылкам справа, чтобы получить те же самые описания с границы Web службы, которые мы видели в предыдущем разделе, и добавить ссылку с помощью кнопки Add Reference Нажатие на эту кнопку приведет к следующим изменениям в Solution Explorer:

Папка, содержащая нашу ссылку Web, называется по имени сервера, где расположена служба, в данном случае — localhost. Это также пространство имен, на которое необходимо ссылаться, чтобы использовать класс прокси, поэтому имеет смысл переименовать папку, что можно сделать с помощью щелчка правой кнопкой мыши на папке. Если переименовать эту папку в myWebService и добавить инструкцию using в код…

using PCSWebClient1.myWebService;

…то тогда можно будет использовать службу в нашем классе.

Добавим обработчик событий к кнопке на форме с помощью следующего кода:

protected void triggerButton_Сlick(object sender, System.EventArgs e) {

Service1 myService = new Service1();

resultLabel.Text = myService.CanWeFixIt();

}

Нажатие кнопки во время выполнения приложения приведет к выводу CanWeFixIt() в окне браузера.

Служба Web может впоследствии измениться, но с помощью такого метода можно просто сделать щелчок правой кнопкой мыши на ссылке Web в проводнике сервера и выбрать Update Web Reference. Это создаст для нас новый класс прокси.

Расширение примера заказа помещения для проведения мероприятий

Теперь, когда мы знакомы с основами создания и использования служб Web, давайте применим наши знания, расширив приложение заказа помещения для проведения мероприятий из предыдущей главы. В частности, извлечем детали доступа к базе данных из приложения и поместим их в службу Web. Эта служба Web имеет два метода:

□ GetData(), который будет возвращать объект DataSet, содержащий все три таблицы базы данных PCSWebApp3.

□ AddEvent(), добавляющий событие и возвращающий обновленную версию DataSet, которая включает изменение

Кроме того, мы создадим службу Web, помня о некоторых приемах сокращения нагрузки из предыдущей главы. В частности, мы будем хранить DataSet на уровне приложения в приложении службы Web. Это означает, что несколько запросов данных не потребуют работы дополнительных запросов к базе данных. Данные в этом DataSet уровня приложения обновятся, когда в базу данных добавятся новые данные. Это означает, что изменения, сделанные в базе данных другими средствами, такими как редактирование вручную, не будут отражаться в этом DataSet. Тем не менее, до тех пор, пока мы знаем, что наша служба Web является единственным местом с прямым доступом к данным, нам не о чем беспокоиться.

Служба Web заказа помещения для проведения мероприятий

Создайте новый проект службы Web в VS.NET с именем PCSWebSrv2. Для начала добавим код в обработчик Application_Start() в global.asax. Мы хотим загрузить все данные из PCSWebApp3.mdb в множество данных и сохранить его. Это будет по большей части включать код, с которым мы знакомы, так как перенос базы данных в DataSet уже делали. Фактически, можно скопировать весь нужный код из WebForm1.aspx.cs в PCSWebApp3 из предыдущей главы, включая строку соединения с базой данных (которая здесь не показана, так как у читателя она должна быть, скорее всего, другой):

protected void Application_Start (Object sender, EventArgs e) {

System.Data.DataSet ds;

System.Data.OleDb.OleDbConnection оleDbConnection1;

System.Data.OleDb.OleDbDataAdapter daAttendees;

System.Data.OleDb.OleDbDataAdapter daRooms;

System.Data.OleDb.OleDbDacaAdapter daEvents;

oleDbConnection1 = new System.Data.OleDb.OleDbConnection();

oleDbConnection1.ConnectionStnng = @" ... ";

oleDbConnection1.Open(); ds = new DataSet();

daAttendees =

new System.Data.OleDb.OleDbDataAdapter(

"SELECT * FROM Attendees", oleDbConnection1);

daRooms =

new System.Data.OleDb.OleDbDataAdapter(

"SELECT * FROM Rooms", oleDbConnection1);

daEvents =

new System.Data.OleDb.OleDbDataAdapter(

"SELECT * FROM Events", oleDbConnection1);

daAttendees.Fill(ds, "Attendees");

daRooms.Fill(ds, "Rooms");

daEvents.Fill(ds, "Events");

oleDbConnection1.Close();

Application["ds"] = ds;

}

Необходимо отметить важный код в последней строке. Объекты Application (и Session) имеют коллекцию пар имя/значение, которую можно использовать для хранения данных. Здесь создается имя ds в хранилище объекта Application, которое получает сериализованное значение DataSet из ds, содержащее таблицы Attendees, Rooms и Events из базы данных. Это значение будет доступно всем экземплярам службы Web в любое время.

Чтобы приведенный выше код работал, нам нужно также добавить ссылку на пространство имен System.Data в пространстве имен PCSWebSrv2 в global.asax:

namespace PCSWebSrv2 {

...

using System.Data;

Эта техника очень полезна для данных, предназначенных только для чтения, так как несколько потоков выполнения смогут к нему обращаться, сокращая нагрузку на базу данных. Отметим, однако, что таблица Events скорее всего должна измениться и будет обновлять DataSet на уровне приложения, когда это произойдет. Мы скоро это увидим.

Затем необходимо добавить к службе в Service1.asmx.cs метод GetData():

[WebMethod]

public DataSet GetData() {

return (DataSet)Application["ds"];

}

Здесь для доступа к множеству данных используется тот же синтаксис, что и в Application_Load(), где просто выполняется преобразование типа данных в правильный тип, а также возврат.

Метод AddEvent() немного сложнее. Концептуально нам необходимо сделать следующее:

□ Получить данные события от клиента.

□ Создать инструкцию SQL INSERT с помощью этих данных.

□ Соединиться с базой данных и выполнить инструкцию SQL.

□ Если добавление выполнится успешно, то обновить данные в Application["ds"].

□ Вернуть уведомление об успехе или отказе клиенту (мы оставляем клиенту возможность обновить его DataSet, если потребуется).

Начиная сверху, принимаем все поля как строки:

[WebMethod]

public int AddEvent(String eventName, String eventRoom, String eventAttendees, String eventDate) {

}

Затем мы объявляем объекты, которые нужны для доступа к базе данных, соединяемся с базой данных и выполняем запрос, используя код аналогичный коду в PCSWebApp3 (здесь также требуется строка соединения, которая здесь не показана):

[WebMethod]

public int AddEvent(String eventName, String eventRoom, String eventAttendees, String eventDate) {

System.Data.OleDb.OleDbConnection oleDbConnection1;

System.Data.OleDb.OleDbDataAdapter dbEvents;

DataSet ds;

oleDbConnection1 = new System.Data.OleDb.OleDbConnection();

OleDbConnection1.ConnectionString = @" ... ";

String oleDbCommand =

"INSERT INTO Events (Name, Room, AttendeeList, " +

" EventDate) VALUES ('" + eventName + "', +

eventRoom + "', '" + eventAttendees + "', '" + eventDate + "')";

System.Data.OleDb.OleDbCommand insertCommand =

new System.Data.OleDb.OleDbCommand(oleDbCommand, oleDbConnection1);

oleDbConnection1.Open();

queryResult = insertCommand.ExecuteNonQuery();

}

Используем, как и прежде, queryResult для хранения числа строк, затронутых запросом. Мы можем проверить его, чтобы оценить наш успех. Если все происходит хорошо, выполняется новый запрос на базе данных для обновления таблицы Events в нашем DataSet. Жизненно важно блокировать данные приложения во время выполнения обновлений, чтобы гарантировать, что никакие другие потоки выполнения не могут получить доступ к Application["ds"] во время его обновления. Это можно сделать с помощью методов Lock() и UnLock() объекта Application:

[WebMethod]

public int AddEvent(String eventName, String eventRoom, String eventAttendees, String eventDate) {

...

int queryResult = insertCommand.ExecuteNonQuery();

if (queryResult == 1) {

daEvents =

new System.Data.OleDb.OleDbDataAdapter(

"SELECT * FROM Events", oleDbConnection1);

Application.Lock();

ds = (DataSet) Application["ds"];

ds.Tables["Events"].Clear();

daEvents.Fill(ds, "Events");

Application["ds"] = ds;

Application.UnLock();

oleDbConnection1.Close();

}

}

Наконец, мы возвращаем queryResult, позволяя клиенту узнать, что запрос был успешным:

[WebMethod]

public int AddEvent(String eventName, String eventRoom, String eventAttendees, String eventDate) {

...

return queryResult;

}

Это завершает создание службы Web. Как и прежде, есть возможность протестировать эту службу, направляя браузер Web на файл .asmx, поэтому мы можем добавить записи и взглянуть на представление XML для DataSet, возвращаемое GetData(), не создавая никакого клиентского кода.

Клиент приложения предварительного заказа помещения для проведения мероприятия

Используемый клиент является разработкой приложения Web PCSWebApp3 из предыдущей главы. Назовем это приложение PCSWebApp4 и воспользуемся кодом из PCSWebApp3в качестве начальной точки.

Сделаем два существенных изменения в проекте. Первое: удалим все непосредственные обращения к базе данных из этого приложения и воспользуемся вместо этого службой Web. Второе: введем хранилище уровня приложения из DataSet, возвращаемого из службы Web, которое обновляется только в случае необходимости, это значит, что на базу данных падает меньшая нагрузка.

Прежде всего в нашем новом приложении Web необходимо добавить ссылку Web на службу PCSWebSrv2/Service1.asmx. Это можно сделать точно таким же образом, как мы видели ранее в этой главе, определяя местонахождение файла .vsdisco и вызывая его eventDataService.

После этого добавляем код в Global.asax, по большей части, таким же образом, как это было сделано для службы Web. Этот код, однако, будет существенно проще. Сначала мы ссылаемся на службу Web и пространство имен System.Data:

namespace PCSWebApp4 {

...

using System.Data;

using eventDataService;

Затем заполняем множество данных (dataset) и помещаем его в хранилище данных уровня приложения с именем ds:

protected void Application_Start(Object sender, EventArgs e) {

Service1 dataService = new Service1();

DataSet ds = dataService.GetData();

Application["ds"] = ds;

}

Теперь DataSet доступно для всех экземпляров PCSWebApp4, т.е. несколько пользователей могут читать данные без какого-либо обращения к службе Web, то есть к базе данных.

Теперь, когда имеется это DataSet, необходимо изменить WebForm1.aspx.cs для его использования. Прежде всего можно удалить объявления oleDbConnection1, daAttendees, daRooms и daEvents, так как не будет осуществляться никакого обращения к базе данных. Затем необходимо изменить Page_Load() следующим образом:

private void Page_Load(object sender, System.EventArgs e) {

validationSummary.Enabled = false;

foreach (System.Web.UI.WebControls.WebControl validator in this.Validators) {

validator.Enabled = false;

}

ds = (DataSet)Application["ds"];

attendeeList.DataSource = ds.Tables["Attendees"];

roomList.DataSource = ds.Tables["Rooms"];

eventTable = ds.Tables["Events"];

eventDetails1.DataSource = eventTable; eventDetails2.DataSource = eventTable;

if (!this.IsPostBack) {

System.DateTime trialDate = System.DateTime.Now;

calendar.SelectedDate = getFreeDate(trialDate);

this.DataBind();

} else {

eventDetails1.DataBind();

eventDetails2.DataBind();

}

}

Большая часть кода остается без изменений, необходимо только использовать Application["ds"] вместо получения DataSet.

Необходимо также изменить submitButton_Click() для использования метода AddData() службы Web. В этом случае также большая часть кода остается без изменений:

protected void submitButton_Click(object sender, System.EventArgs e) {

foreach (System.Web.UI.WebControls.WebControl validator in this.Validators) {

validator.Enabled = true;

}

this.Validate();

if (this.IsValid) {

String attendees = "";

foreach (ListItem attendee in attendeeList.Items) {

if (attendee.Selected) {

attendees += attendee.Text + " (" + attendee.Value + "), ";

}

}

attendees += " and " + nameBox.Text;

String dateString = calendar.SelectedDate.Date.Date.ToShortDateString();

Service1 dataService = new Service1();

int queryResult =

dataService.AddEvent(eventBox.Text, roomList.SelectedItem.Value,

attendees, dateString);

if (queryResult == 1) {

resultLabel.Text = "Event Added";

ds = dataService.GetData();

Application.Lock();

Application["ds"] = da;

eventTable = ds.Tables["Events"];

calendar.SelectedDate = getFreeDate(calendar.SelectedDate.AddDays(1));

eventDetails1.DataSource = eventTable;

eventDetails1.DataBind();

eventDetails2.DataSource = eventTable;

eventDetails2.DataBind();

} else {

resultLabel.Text = "Event not added due to DB access problem.";

}

} else {

validationSummary.Enabled = true;

}

}

Фактически, мы существенно упростили систему. Это часто бывает при правильном проектировании служб Web — можно забыть о большей части работы приложения и вместо этого сосредоточиться на работе пользователя.

К этому коду почти не требуется комментариев. Продолжающееся использование queryResult является дополнительной премией, а блокирование приложения существенно, как уже было отмечено.

Приложение Web PCSWebApp4 выглядит и функционирует точно также, как PCSWebApp3, но выполняется существенно лучше. Можно также очень легко использовать эту же службу Web для других приложений, просто выводя на странице мероприятия, например, или редактируя мероприятия, имена почетных гостей и помещения, если добавить несколько других методов. Все это не разрушит PCSWebApp4, так как мы будем просто игнорировать все вновь созданные методы.

Заключение

В этой главе мы увидели, как создавать и использовать службы Web с помощью C# и платформы разработки VS.NET. Сделать это достаточно просто, но какая это невероятно полезная возможность. Уже сейчас мы видим множество объявлений о новых службах Web и можно ожидать, что скоро они будут повсюду.

Также было отмечено, что службы Web могут быть доступны с любой платформы. Это связано с простотой протокола SOAP, который не ограничивается платформой .NET.

Пример, разработанный в этой главе, иллюстрирует, как можно легко создавать распределенные приложения .NET. Здесь предполагается, что для тестирования используется один сервер, но почему бы службу Web полностью не отделить от клиента. Она может даже находиться на отдельном от базы данных сервере, если требуется дополнительная связь данных.

Использование кэширования данных является другой важной техникой, которую необходимо освоить для использования в крупномасштабных приложениях, когда имеются тысячи одновременно присоединяющихся пользователей. Конечно, в такой ситуации использование Microsoft Access в качестве источника данных может оказаться не лучшей идеей.

Наконец, стоит помнить, что потребителями службы Web не обязательно должны быть приложения Web. Нет причин, по которым нельзя использовать службы Web из приложений платформы Windows, что может оказаться привлекательным для корпоративных сетей интранет.

Глава 18

Специальные элементы управления

При разработке приложений Web случаются ситуации, когда доступные мощные инструменты разработки не вполне соответствуют требованиям для определенного проекта. Возможно, что заданный элемент управления работает не вполне так, как это требуется, или раздел кода, предназначенный для повторного использования на нескольких страницах, слишком сложен в руках нескольких разработчиков. В таких ситуациях существуют серьезные доводы в пользу специальных элементов управления. Специальные элементы управления могут, в самом простейшем виде объединить несколько существующих элементов управления вместе, возможно, с дополнительными свойствами, определяющими компоновку, или могут полностью отличаться от любых существующих элементов управления. Использование специального элемента управления настолько же просто, как и использование любого другого элемента управления в ASP.NET, что, конечно, облегчает кодирование web-сайтов.

В прошлом было сложно реализовывать такие специально созданные элементы управления, особенно в крупномасштабных системах, где требовались сложные процедуры регистрации для их использования. Даже в простых системах требования кодирования для создания специального элемента управления могли существенно усложнить процесс. Языки сценариев старых языков разработки Web также страдали от отсутствия возможности предоставить полноценный доступ к искусно созданной объектной модели, что приводило к низкой производительности в целом.

Платформа .NET предоставляет идеальное окружение для создания специальных элементов управления. Динамическое обнаружение сборок, что свойственно системе .NET делает установку на новом сервере Web посредством копирования структуры каталогов, содержащей код, вместе со всеми используемыми DLL. Кроме того, специальное внимание было уделено упрощению создания своих собственных элементов управления с помощью простых технологий программирования.

В этой главе мы рассмотрим два различных вида элементов управления:

□ Элементы управления пользователя — преобразование существующих страниц ASP.NET в элементы управления

□ Специальные элементы управления — объединение функций нескольких элементов управления, расширение существующих элементов управления и создание новых элементов управления с самого начала

Мы проиллюстрируем элементы управления пользователя, преобразовывая приложение предварительного заказа помещения для проведения мероприятий из предыдущей главы в элемент управления пользователя так, чтобы его можно было легко встроить в другие страницы ASP.NET. В случае специальных элементов управления мы создадим элемент управления для выборочного опроса общественного мнения, позволяющий пользователю голосовать за какой-то вариант из списка и видеть, как проходит голосование.

Элементы управления пользователя

Элементы управления пользователя создаются с помощью кода ASP.NET также, как создаются стандартные страницы Web ASP.NET. Различие состоит в том, что после создания элемента управления пользователя его можно повторно применять на множестве страниц ASP.NET с минимальными трудностями.

Например, предположим, что создана страница, которая выводит некоторую информацию из базы данных, возможно, информацию о заказе. Вместо создания фиксированной страницы, которая это делает, можно поместить соответствующий код в элемент управления пользователя и затем вставлять при желании этот элемент управления в множество различных страниц Web.

Кроме того, можно определить свойства и методы для элементов управления пользователя. Например, определить фоновый цвет для вывода таблицы базы данных на странице Web или повторно выполнить запрос к базе данных для проверки изменений.

Давайте создадим простой элемент управления пользователя, обсуждая соответствующие вопросы по мере их появления, и затем скомпонуем его, чтобы увидеть, как можно добавлять методы и свойства.

Простой элемент управления пользователя

В VS.NET создадим новое приложение Web с именем PCSUserCWebAppl, открывая VS.NET, щелкнув на Getting Started, и выбирая затем New Project, щелкнув на пиктограмме Web Application. Должно открыться диалоговое окно, позволяющее сохранить этот проект.

Когда будут созданы стандартные файлы, выберем пункт меню Project|Add New Item… и добавим Web User Control с именем PCSUserC1.ascx, как показано ниже:

Добавленные к проекту файлы с расширениями .ascx и .ascx.cs работают очень похожим образом с файлами .aspx, с которыми мы уже знакомы. Файл .ascx будет содержать код ASP.NET и выглядеть очень похоже на обычный файл .aspx. Файл .ascx.cs является нашим кодом, который определяет элемент управления пользователя преимущественно так же, как в файлах .aspx.cs определяются формы.

Файлы .ascx можно просматривать в виде кода HTML или в окне построителя, как и файлы .aspx. Просмотр файла в виде кода HTML открывает важное различие: в элементе <body> отсутствует элемент <form>. Это связано с тем, что элементы управления пользователя будут вставляться внутрь форм ASP.NET в других файлах, и поэтому не нуждаются в собственном теге формы.

Просмотр созданных шаблонных файлов открывает еще одно важное отличие: созданный класс наследует из класса System.Web.UI.UserControl. Это также связано с тем, что элемент управления будет использоваться внутри формы.

Наш простой элемент управления будет выводить графическое изображение, соответствующее одной из четырех стандартных мастей колоды карт (трефы, бубны, черви, пики). Требуемые для этого графические изображения поставляются как часть Visual Studio; их можно найти в C:Program Files\Microsoft Visual Studio.NET\Common7\Graphics\bitmaps\assorted с именами файлов CLUB.BMP, DIAMOND.BMP, HEART.BMP и SPADE.BMP. Скопируйте их в каталог проекта, чтобы ими можно было воспользоваться.

Добавим некоторый код к новому элементу управления. В файл PCSUserC1.ascx, представленный в виде кода HTML, добавим следующие строки:

<HTML>

<HEAD> </HEAD>

<BODY>

<TABLE CellSpacing=4>

<TR vAlign=middle>

<TD>

<asp:Image Runat="server" ID="suitPic" ImageURL="club.bmp " />

</TD>

<TD height=20>

<asp:Label Runat="server" ID="suitLabel">Club</asp:Label>

</TD>

</TR>

</TABLE>

</BODY>

</HTML>

Этот код определяет состояние по умолчанию элемента управления, которое будет изображением трефы с меткой. Прежде чем добавлять дополнительную функциональность, протестируем такое поведение по умолчанию, добавляя этот элемент управления в проект Web-страницы WebForm1.aspx.

Чтобы использовать специальный элемент управления в файле .aspx, сначала необходимо определить, как мы будем на него ссылаться, то есть, имя тега, который будет представлять элемент управления в коде HTML. Чтобы сделать это, используется директива <%@ Register %> в верхней части кода следующим образом:

<%@ Register TagPrefix="PCS" TagName="UserC1" Src="PCSUserC1.ascx" %>

Здесь используются атрибут Src для указания на файл, содержащий элемент управления пользователя, и атрибуты TagPrefix и TagName для определения имени тега для использования (в форме TagPrefix: TagName). Теперь мы можем использовать этот элемент управления, добавляя следующий элемент:

<%@ Page language="c#" Codebehind="WebForm1.aspx.cs" AutoEventWireup="false"

Inherits="PCSUserCWebApp1.WebForm1" %>

<%@ Register TagPrefix="PCS" TagName="UserC1" Src= "PCSUserC1.ascx" %>

<HTML>

<HEAD>

<meta name=vs_targetSchema content="Internet Explorer 5.0">

<meta name="GENERATOR" Content="Microsoft Visual Studio 7.0">

<meta name="CODE_LANGUAGE" Content="C#" >

</HEAD>

<BODY MS_POSITIONING="GridLayout">

<form method="post" runat="server">

<PCS:UserC1 Runat="server" id="myUserControl" />

</form>

</BODY>

</HTML>

Элементы управления пользователя могут не объявляться по умолчанию в базовом коде формы, поэтому может понадобиться добавить следующее объявление в WebForm1.aspx.cs:

public class WebForm1 : System.Web.UI.Page {

protected PCSUserC1 myUserControl;

...

Это все, что нужно сделать для тестирования элемента управления пользователя, и выполнение проекта приведет к следующему результату:

club

Этот элемент управления группирует вместе два существующих элемента управления, изображение и метку в табличной компоновке. Поэтому он попадает в категорию композитных элементов управления.

Чтобы получить управление над выводимой мастью, можно использовать атрибут элемента <PCS: UserC1>. Атрибуты элементов для элементов управления пользователя автоматически отображаются в свойства элементов управления пользователя, поэтому для того, чтобы это заработало, необходимо только добавить свойство в код элемента управления PCSUserC1.ascx.cs. Назовем это свойство Suit и позволим ему принимать значение любой масти. Чтобы упростить представление состояния элемента управления, определим внутри пространства имен PCSUserCWebAppl перечисление для хранения четырех названий мастей:

namespace PCSUserCWebAppl {

...

public enum suit {

club, diamond, heart, spade

}

...

}

Для класса PCSUserC1 требуется переменная-член для хранения типа данных suit (масть) — currentSuit:

public class PCSUserC1 : System.Web.UI.UserControl {

protected System.Web.UI.WebControls.Image suitPic;

protected System.Web.UI.WebControls.Label suitLabel;

protected suit currentSuit;

А также свойство для доступа к этой переменной-члену, Suit:

public suit Suit {

get {

return currentSuit;

}

set {

currentSuit = value;

suitPic.ImageUrl = currentSuit.ToString() + ".bmp";

suitLabel.Text = currentSuit.ToString();

}

}

Здесь метод доступа set() задает URL изображения как один из файлов, скопированных ранее, а текст выводит название масти.

Теперь элемент управления закончен, и нам надо добавить код в WebForm1.aspx для доступа к этому новому свойству. Используем список переключателей для выбора масти:

<BODY MS_POSITIONING="GridLayout">

<form method="post" runat="server">

<PCS:UserC1 Runat="server" id="myUserControl" />

<asp:RadioButtonList Runat="server" ID="suitList" autopostback="True">

<asp:ListItem Value="club" Selected="True">Club</asp:ListItem>

<asp:ListItem Value="diamond">Diamond</asp:ListItem>

<asp:ListItem Value="heart">Heart</asp:ListItem>

<asp:ListItem Value="spade">Spade</asp:ListItem>

</asp:RadioButtonList>

</form>

</BODY>

Нам нужно также добавить обработчик событий для события списка SelectedIndexChanged, который мы можем сделать просто с помощью двойного щелчка мышью на элементе управления в графическом представлении.

Отметим, что нужно задать свойство autopostback этого списка как true, так как обработчик события suitList_SelectedIndexChanged() не будет выполняться на сервере, если обратная отправка не задана, и этот элемент управления не включает обратную отправку по умолчанию.

Для метода suitList_SelectedIndexChanged() требуется следующий код в WebForm1.aspx.cs:

protected void suitList_SelectedIndexChanged(object sender, System.EventArgs e) {

MyUserControl.Suit = (suit)Enum.Parse(typeof(suit), suitList.SelectedItem.Value);

}

Мы знаем, что атрибуты value элементов <ListItem> представляют допустимые значения перечисления suit, которое было определено ранее, поэтому мы анализируем их просто как типы перечислений (у нас здесь то же пространство имен, поэтому нам не нужно переопределять тип) и используем их как значения свойства Suit элемента управления пользователя. Мы преобразуем возвращаемый тип object в suit с помощью простого синтаксиса преобразования типов, и это невозможно сделать неявно.

Не нужно это усложнять, просто определим одно значение с помощью атрибута Suit формы Web, например:

<PCS:UserC1 Runat="server" id="myUserControl" Suit="diamond" />

Процессор ASP.NET достаточно разумен, чтобы получить правильный элемент перечисления из предоставленной строки:

Теперь можно изменять масть при выполнении этого приложения Web:

Затем мы зададим для элемента управления несколько методов. Это снова сделать несложно, нам нужно только добавить методы в класс PCSUserC1:

public void Club() {

Suit = suit.club;

}

public void Diamond() {

Suit = suit.diamond;

}

public void Heart() {

Suit = suit.heart;

}

public void Spade() {

Suit = suit.spade;

}

Эти четыре метода — Club(), Diamond(), Heart() и Spade() — изменяют выведенную на экран масть на ту, которая была указана.

Мы вызываем эти функции из четырех элементов управления на странице .aspx:

<asp:ImaqeButton Runat="server" ID="clubButton"

ImageUrl="CLUB.BMP" OnClick="clubButton_OnClick" />

<asp:ImageButton Runat="server" ID="diamondButton"

ImageUrl="DIAMOND.BMP" OnСlick="diamondButton_OnClick" />

<asp:ImageButton Runat="server" ID="heartButton"

ImageUrl="HEART.BMP"  OnClick="heartButton_OnClick" />

<asp:ImageButton Runat="server" ID="spadeButton"

ImageUrl="SPADE.BMP" OnClick="spadeButton_OnClick" />

</form>

С помощью следующих обработчиков событий:

protected void clubButton_OnClick(object sender, System.Web.UI.ImageClickEventArgs e) {

myUserControl.Club()

}

protected void diamondButton_OnClick(object sender, System.Web.UI.ImageClickEventArgs e) {

myUserControl.Diamond();

}

protected void heartButton_OnClick(object sender, System.Web.UI.ImageClickEventArgs e) {

myUserControl.Heart();

}

protected void spadeButton_OnClick(object sender, System.Web.UI.ImageClickEventArgs e) {

myUserControl.Spade();

}

Теперь мы имеем четыре новые кнопки, которые можно использовать для изменения масти:

Отметим, что эти кнопки не изменяют выбранный переключатель, хотя сделать это было бы достаточно просто.

Теперь, создав элемент управления пользователя, можно использовать его на любой другой странице Web с помощью директивы <%@ Register %> и двух файлов исходного кода (PCSUserC1.ascx и PCSUserC1.ascx.cs), созданных для элемента управления.

Преобразование приложения предварительного заказа мероприятия в элемент управления пользователя

В большинстве случаев преобразование страницы ASP.NET в элемент управления пользователя выполнить легко, так как можно просто скопировать требуемый код в пустые файлы .ascx и ascx.cs. Можно даже выйти из положения в некоторых случаях, просто изменяя имя файла на .ascx, если поместить весь код C# в этот файл, а не использовать режим "code behind".

Однако в некоторых случаях это невозможно, так как существуют препятствия, которые необходимо рассмотреть, чтобы преобразовать приложение Web из предыдущей главы в элемент управления пользователя.

Это приложение использует переменную уровня приложения для множества данных, которая содержит таблицы данных мероприятий, участников и названий помещений. Если желательно использовать эту переменную таким же образом, нам понадобиться в этом проекте поместить код для извлечения множества данных в файле global.asax. То есть нам еще нужно добавить в проект ссылку Web на требуемую службу Web.

Существуют другие изменения, которые необходимо сделать, чтобы учесть факт, что элемент управления имеет базовый класс UserControl, а не Form. Например, UserControl не имеет коллекции Validators, поэтому невозможно просмотреть объекты Validator в этой коллекции с помощью кода, который использовался ранее:

protected void submitButton_click(object sender, System.EventArgs e) {

foreach (System.Web.UI.WebControls.WebControl validator in this.Validators) {

validator.Enabled = true;

}

this.Validate();

if (this.IsValid) {

...

Вместо этого необходимо использовать следующий подход:

protected void submitButton_Click(object sender, System.EventArgs e) {

validateEvent.Enabled = true;

validateRoom.Enabled = true;

validateName.Enabled = true;

validateAttendees.Enabled = true;

validateEvent.Validate();

validateRoom.Validate();

validateName.Validate();

validateAttendees.Validate();

if (validateAttendees.IsValid && validateEvent.IsValid &&

validateRoom.IsValid && validateName.IsValid) {

...

Существует также проблема с тем. что элемент управления использует итоговую проверку. Значит, другие области на странице, использующей этот элемент управления, будут требовать дополнительной работы для проверки. Это связано с тем, что итоговая проверка работает для всей страницы, включая все элементы управления проверкой, несмотря на то, где они находятся, поэтому может возникнуть путаница. Возможно, простейшим решением здесь является создание собственного обработчика проверки, который использует элементы управления проверкой только в элементе управления пользователя, один из них — элемент управления меткой, с помощью которого выводится требуемый текст вручную. Но здесь этот подход рассматриваться не будет.

В целом необходимо сделать все элементы управления пользователя полностью независимыми, что по сути означает, что данное приложение не является идеальным. Однако оно высветило некоторые ограничения элементов управления пользователя.

В следующем разделе мы увидим, как можно изменять элементы управления в значительно большей степени с помощью синтаксиса специальных элементов управления.

Специальные элементы управления

Специальные элементы управления — следующий шаг по сравнению с элементами управления пользователя в том смысле, что они являются полностью самодостаточными в сборках C#, не требуя отдельного кода ASP.NET. Это означает, что нам не нужно проходить через процесс сборки UI в файле .ascx. Вместо этого мы имеем полный контроль над тем, что записывается в поток вывода, то есть, над кодом HTML, созданным элементом управления.

Обычно для разработки специального элемента управления требуется больше времени, чем для элемента управления пользователя, так как синтаксис является более сложным и часто требуется написать значительно больший код, чтобы получить результат. Элемент управления пользователя, как мы видели, может просто объединять несколько других элементов управления, в то время как специальный элемент управления может делать практически что угодно.

Чтобы получить наиболее гибкое поведение специальных элементов управления, можно выводить класс из System.Web.UI.WebControls.WebControl. В этом случае создается полный специальный элемент управления. Иначе можно расширить функциональность существующего элемента управления, создавая производный специальный элемент управления. Наконец, можно сгруппировать существующие элементы управления почти так, как это делалось в предыдущем разделе, но с более логичной структурой, чтобы создать композитный специальный элемент управления:

Любой из этих элементов может использоваться на страницах ASP.NET одинаково. Необходимо только поместить созданную сборку в каталог bin приложения Web, которое будет его использовать, и зарегистрировать имена используемых элементов с помощью директивы <%@ Register %>. В этой директиве применяется немного другой синтаксис для специальных элементов управления:

<%@ Register TagPrefix="PCS" Namespace="PCSCustomWebControls" Assembly="PCSCustomWebControls" %>

Мы используем параметр TagPrefix таким же образом, как и раньше, но не используем атрибуты TagName или Src. Это связано с тем, что сборка специального элемента управления может содержать несколько специальных элементов управления, и каждый из них будет именован согласно своему классу, поэтому TagName является лишним. Кроме того, так как сборка находится в каталоге bin, мы можем использовать средства платформы .NET для динамического обнаружения требуемой сборки просто по имени и пространству имен в ней, которое содержит элементы управления.

Выше, в примере строки кода, мы говорим, что хотим использовать сборку с именем PCSCustomWebControls.dll с элементами управления в пространстве имен PCSCustomWebControls, и при этом используем префикс PCS. Если в этом пространстве имен имеется элемент управления с именем Control1, то можно использовать его с кодом ASP.NET:

<PCS:Control1 Runat="server" ID="MyControl" />

С помощью специальных элементов управления можно также воспроизвести некоторое вложенное поведение элементов управления, такое, как мы видим в списке элементов управления:

<asp:dropdownlist id="roomList" runat="server" width="160px">

<asp:ListItem Value="1">The Happy Room</asp:ListItem>

<asp:ListItem Value="2">The Angry Room</asp:ListItem>

<asp:ListItem Value="3">The Depressing Room</asp:ListItem>

<asp:ListItem Value="4">The Funked Out Room</asp:ListItem>

</asp:dropdownlist>

Можно создать очень похожим образом элементы управления, которые интерпретируются как потомки других элементов управления. Позже в этой главе мы увидим, как это делается.

Конфигурация проекта специального элемента управления

Применим часть этой теории на практике. Мы будем использовать для простоты единственную сборку для хранения всех специальных элементов управления примера этой главы, которую можно создать в Visual Studio.NET, выбирая новый проект типа Web Control Library. Назовем нашу библиотеку PCSCustomWebControls:

Здесь проект создан в каталоге wwwroot, хотя это и не обязательно. Библиотеки элементов управления Web можно создавать где угодно, необходимо только скопировать созданную сборку в каталог bin приложения Web, которое ее использует.

Один из технических приемов, применяемых для упрощения тестирования одиночного решения, состоит в добавлении проекта приложения Web к тому же решению:

В данный момент это единственное приложение, которое будет использовать нашу библиотеку специального элемента управления, поэтому для ускорения работы определим, что выводимая сборка библиотеки создается в правильном каталоге bin (то есть нам не нужно копировать файл после каждой перекомпиляции). Это можно сделать с помощью страниц свойств проекта PCSCustomWebControls:

Отметим, что здесь в раскрывающемся списке Configuration выбран элемент All Configurations, поэтому отладочная и окончательная сборка будут помещены в одном месте. Output Path был изменен на C:\Inetpub\wwwroot\PCSCustomWebControlsTestApp\bin Чтобы облегчить отладку можно также изменить значение Start URL на странице свойств Debugging на http://localhost/PCSCustomWebControlsTestApp/WebForm1.aspx, a Debug Mode - на URL таким образом, чтобы увидеть результаты, проект можно выполнять просто в режиме отладки.

Убедимся, что все это работает, протестировав элемент управления, который поставляется по умолчанию в файле .cs для библиотеки специального элемента управления, называемой WebCustomControl1. Нам нужно внести следующие изменения в код WebForm1.aspx, который просто ссылается на вновь созданную библиотеку элемента управления и встраивает используемый по умолчанию элемент из этой библиотеки в тело страницы:

<%@ Page language="c#" Codebehind="WebForm1.aspx.cs"

AutoEventWireup="false" Inherits="PCSCustomWebControlsTestApp.WebForm1" %>

<%@ Register TagPrefix="PCS" Namespace="PCSCustomWebControls"

Assembly="PCSCustomWebControls" %>

<html>

<head>

<meta name="GENERATOR" Content="Microsoft Visual Studio 7.0">

<meta name="CODE_LANGUAGE" Content = "C#">

<meta name=vs_defaultClientScript content="JScript">

<meta name=vs_targetSchema content="Internet Explorer 5.0">

</head>

<body MS_POSITIONING="GridLayout">

<form id="WebForm1" method="post" runat="server">

<PCS:WebCustomControl1 Runat="server" Text="Testing again..." />

</form>

</body>

</html>

Теперь, пока библиотека PCSCustomWebControls сконфигурирована как приложение запуска, можно нажать кнопку Debug, чтобы увидеть результаты работы:

Добавим также ссылку на проект PCSCustomWebControls в раздел тестирования приложений:

Затем добавим инструкцию using в пространство имен PCSCustomWebControlsTestApp в WebForm1.aspx.cs:

using PCSCustomWebControls;

Это позволит нам использовать наши специальные элементы управления из кода, скрытого за формой.

Базовые специальные элементы управления

Как можно предположить из результатов предыдущего раздела, образец элемента управления, создаваемый по умолчанию, является версией стандартного элемента управления <asp:Labels>. Создаваемый в файле .cs код проекта, WebCustomControl1.cs, выглядит следующим образом:

namespace PCSCustomWebControls {

using System;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.ComponentModel;

/// <summary>

/// Краткое описание WebCustomControl1

/// </summary>

[DefaultProperty("Text"),

ToolboxData("<{0}WebCustomControl1 runat=server></{0}:WebCustomControl1>")]

public class WebCustomControl1 : System.Web.UI.WebControls.WebControl {

private string text;

[Bindable(true), Category("Appearance"), DefaultValue(" ")]

public string Text {

get {

return text;

}

set {

text = value;

}

}

/// <summary>

/// Предоставить этот элемент управления указанному параметру вывода.

/// </summary>

/// <param name="output"> The HTML writer to write out to </param>

protected override void Render(HtmlTextWriter output) {

output.Write(Text);

}

}

}

Начальные инструкции using для пространств имен вполне стандартны.

Здесь определен единственный класс WebCustomControl1 (отметим, как имя класса отображается прямо в элемент ASP.NET в простом примере, только что увиденном), который является производным из класса WebControl, как обсуждалось ранее. Для этого класса предоставлены два атрибута: DefaultProperty и ToolboxData. Атрибут DefaultProperty определяет, какое свойство будет использоваться по умолчанию для элемента управления в языках, которые поддерживают эту функциональность. Атрибут ToolboxData точно определяет, какой код HTML будет добавлен к странице .aspx, если этот элемент управления добавляется с помощью инструментальной панели Visual Studio (когда проект откомпилирован, можно добавить элемент управления в панель инструментов, конфигурируя панель инструментов для использования созданной сборки).

Класс содержит одно свойство: Text. Это очень простое текстовое свойство, похожее на те, которые встречались раньше. Здесь необходимо отметить только три атрибута:

□ Bindable — показывает, может ли свойство быть связано с данными.

□ Category — задает, будет ли свойство выводиться на страницах свойств.

□ DefaultValue — значение по умолчанию для свойства.

Представление свойств в таком виде работает точно таким же образом, как и для специальных элементов управления, и определенно предпочтительно по отношению к представлению открытых полей.

Остальная часть класса состоит из метода Render(). Это единственный самый важный метод для реализации при создании специальных элементов управления, так как в нем мы получаем доступ к потоку вывода для изображения содержимого элемента управления. Существует только два случая, когда этот метод не нужно реализовывать:

□ Когда создается элемент управления, не имеющий визуального представления (обычно называемый компонентом).

□ Когда создается производный элемент из существующего элемента управления и не требуется изменять характеристики его изображения.

Специальные элементы управления могут также предоставлять специальные методы, инициировать специальные события, и отвечать производным элементам управления (если они существуют). Ниже мы рассмотрим:

□ Создание производных элементов управления

□ Создание композитных элементов управления

□ Создание более развитых элементов управления

Конечный пример будет элементом управления выбором, который разрешает пользователю голосовать за одну из нескольких возможностей, и выводить графически результаты голосования. Варианты выбора будут определяться с помощью вложенных элементов управления потомков способом, описанным ранее.

Создадим для начала простой производный элемент управления.

Производный элемент управления RainbowLabel

Для этого первого примера создадим производный элемент управления из элемента управления Label и переопределим его метод Render() для вывода многоцветного текста. Чтобы держать элементы управления примера в этой главе отдельно, создадим новые файлы исходного кода, поэтому для этого элемента управления создадим новый файл .cs с именем RainbowLabel.cs и введем в него следующий код:

namespace PCSCustomWebControls {

using System;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.ComponentModel;

using System.Drawing;

public class RainbowLabel : System.Web.UI.WebControls.Label {

private Color[] colors = new Color[] {

Color.Red, Color.Orange, Color.Yellow,

Color.GreenYellow, Color.Blue, Color.Indigo, Color.Violet

};

protected override void Render(HtmlTextWriter output) {

string text=Text;

for (int pos=0; pos < text.Length; pos++) {

int rgb = colors[pos % 7].ToArgb() & 0xFFFFFF;

output.Write("<font color="#" + rgb.ToString("X6") + "'>" + text[pos] + "</font>");

}

}

}

}

Этот класс выводится из существующего элемента управления Label (System.Web.UI.WebControls.Label) и не требует никаких дополнительных свойств, так как достаточно унаследованного свойства Text. Мы добавили новое скрытое поле — colors[], которое содержит массив цветов, циклически изменяющихся при выводе текста.

Основная функциональность элемента управления находится в Render(), который переопределен, так как мы хотим изменить вывод HTML. Здесь мы берем строку для вывода из свойства Text и выводим каждый символ цветом из массива colors[].

Чтобы протестировать этот элемент управления, необходимо добавить его к форме в PCSCustomWebControlsTestApp:

<form method="post" runat="server" ID="Form1">

<PCS:RainbowLabel Runat="server" Text="Multicolored label!"

ID="rainbowLabel1" />

</form>

Нам нужно также добавить подходящее объявление в код, реализующий форму (если оно не добавится автоматически):

public class WebForm1 : System.Web.UI.Page {

protected RainbowLabel rainbowLabel1;

...

В результате будет получено:

Поддержание состояния в специальном элементе управления

Каждый раз при создании элемента управления на сервере в ответ на запрос к серверу, он создается с самого начала. Это означает что любое простое поле элемента управления будет повторно инициализироваться. Чтобы элементы управления поддерживали состояние между запросами, они должны использовать ViewState, о чем требуется помнить при создании элементов управления.

Чтобы проиллюстрировать это, добавим дополнительное свойство в элемент управления RainbowLabel. Мы добавив метод с именем Cycle(), который циклически перебирает доступные цвета и использует хранимое поле offset для определения цвета первой буквы выводимой строки.

Это поле должно использовать ViewState элемента управления, чтобы быть устойчивым между запросами. Если это не сделано и поле инициализируется в элементе управления, то все будет работать неправильно.

Здесь будет показан код для обоих случаев, чтобы увидеть ловушку, в которую очень легко попасть. Сначала мы посмотрим на код, который не может воспользоваться ViewState:

public class RainbowLabel : System.Web.UI.WebControls.Label {

private Color[] colors = new Color[] {

Color.Red, Color.Orange, Color.Yellow,

Color.GreenYellow, Color.Blue, Color.Indigo, Color.Violet

};

private int offset = 0;

protected override void Render(HtmlTextWriter writer) {

string text = Text;

for (int pos = 0; pos < text.Length; pos++ ) {

int rgb = colors[(pos + offset) % 7].ToArgb() & 0xFFFFFF;

output.Write("<font color= '#" + rgb.ToString("X6") + "'>" + text[pos] + "</font>");

}

}

public void Cycle() {

offset = ++offset % 7;

}

}

Здесь мы инициализируем поле offset нулем, а затем позволяем методу Cycle() увеличивать его. Использование оператора % гарантируем, что оно уменьшится до 0, если достигнет 7.

Чтобы протестировать это, требуется способ вызова метода Cycle() и добавление кнопки к форме:

<form method="post" runat="server" ID="Form1">

<PCS:RainbowLabel Runat="server" Text="Multicolored label!"

ID="rainbowLabel1" />

<asp:Button Runat="server" ID="cycleButton"

Text="Cycle colors" OnClick="cycleButton_Click" />

</form>

Co следующим обработчиком событий:

protected void cycleButton_Click(object sender, System.EventArgs e) {

this.rainbowLabel1.Cycle();

}

Если выполнить этот код, то окажется, что цвета изменяются при первом нажатии кнопки, но дальнейшие нажатия будут оставлять цвета без изменений.

Если этот элемент управления сохраняет себя на сервере между запросами, то он работает правильно, так как поле offset поддерживает свое состояние, не требуя специального внимания. Однако эта техника не имеет смысла для приложения Web, когда потенциально тысячи пользователей используют его одновременно. Создание отдельного экземпляра для каждого пользователя только ухудшает ситуацию.

В любом случае решение достаточно просто. Мы должны использовать свойство ViewState элемента управления для сохранения и извлечения данных. Нам не нужно беспокоиться о том, как это сериализуется, создается заново или о чем-то еще, мы просто помещаем данные в свойство и извлекаем данные, уверенные, что состояние будет поддерживаться между запросами стандартными средствами ASP.NET.

Чтобы поместить поле offset в ViewState, мы используем следующий код:

ViewState["_offset"] = offset;

ViewState состоит из пар имя-значение, и в данном случае используется имя _offset. Нам не нужно объявлять его где-либо, оно будет создано при первом использовании этого кода.

Аналогично для извлечения состояния используется код:

offset = (inc)ViewState["_offset"];

Если мы сделаем это, когда ничего не хранится в ViewState под этим именем, то будет получено значение null. Простейший способ справиться с такой ситуацией — использовать вызов в блоке try.

Собирая все вместе, сделаем следующие изменения в коде:

public class RainbowLabel : System.Web.UI.WebControls.Label {

private Color[] colors = new Color[] {

Color.Red, Color.Orange, Color.Yellow,

Color.GreenYellow, Color.Blue, Color.Indigo, Color.Violet

};

private int Offset;

protected override void Render(HtmlTextWriter writer) {

string text = Text;

GetOffset();

for (int pos = 0; pos < text.Length; pos++) {

int rgb = colors[(post + offset) % 7].ToArgb() & 0xFFFFFF;

writer.Write("<font color='#" + rgb.ToString("X6") + "'>" + text[pos] + "</font>");

}

}

private void GetOffset() {

try {

offset = (int)ViewState["_offset"],

} catch {

offset = 0;

}

}

public void Cycle() {

GetOffset();

offset = ++offset % 7;

ViewState["_offset"] = offset;

}

}

Теперь элемент управления позволит методу Cycle() работать каждый раз. Обычно ViewState используется для простых свойств, таких как свойства string:

public string Name {

get {

return (string)ViewState["_name"];

}

set {

ViewState["_name"] = value;

}

}

Еще одно замечание об использовании ViewState имеет отношение к элементам управления-потомкам. Если элемент управления имеет потомков и используется на странице больше одного раза, то возникает проблема, связанная с тем, что потомки будут по умолчанию сообща использовать свой ViewState. Практически в каждом случае это не то поведение, которое требуется, и к счастью мы имеем простое решение. Выводя элемент управления-предок из INamingContainer мы вынуждаем элементы управления-потомки использовать квалифицированное хранилище в ViewState, так что элементы управления-потомки не будут сообща использовать свой ViewState с аналогичными элементами управления-потомками с другим предком.

Использование этого интерфейса не требует никакой дополнительной реализации, нам нужно просто сказать, что мы его используем, как если бы это был просто маркер для интерпретации сервером ASP.NET. Мы сделаем это в следующем разделе.

Создание композитного специального элемента управления

В качестве несложного примера композитного специального элемента управления можно объединить элемент управления из предыдущего раздела с кнопкой цикла, которая имелась на тестовой форме.

Назовем этот композитный элемент управления RainbowControl2 и поместим его в новый файл RainbowControl2.cs. Этот элемент управления должен делать следующее:

□ Наследовать из WebControl (а не от Label в этот раз)

□ Поддерживать INamingContainer

□ Иметь два поля для хранения своих элементов управления-потомков

public class RainbowLabel2 : System.Web.UI.WebControls.WebControl, INamingContainer {

private RainbowLabel rainbowLabel = new RainbowLabel();

private Button cycleButton = new Button();

Чтобы сконфигурировать композитный элемент управления, необходимо сделать так, чтобы всякий элемент управления-потомок добавлялся к коллекции Controls и правильно инициализировался. Мы делаем это, переопределяя метод CreateChildControls() и помещая туда необходимый код:

protected override void CreateChildControls() {

cycleButton.Text = "Cycle colors.";

cycleButton.Click += new System.EventHandler(cycleButton_Click);

Controls.Add(cycleButton);

Controls.Add(rainbowLabel);

}

Здесь мы используем только метод Add() коллекции Controls, чтобы работа была корректной. Добавляем также обработчик событий для кнопки, чтобы мы могли циклически менять цвета, это достигается точно таким же образом, как и для других событий. Обработчик событий уже знаком:

protected void cycleButton_Click(object sender, System.EventArgs e) {

rainbowLabel.cycle();

}

Данный вызов заставляет цвета метки циклически изменяться.

Чтобы предоставить пользователям композитного элемента управления доступ к тексту в потомке rainbowLabel, можно добавить свойство, которое отображается в свойство Text потомка:

public string Text {

get {

return rainbowLabel.Text;

}

set {

rainbowLabel.Text = value;

}

}

Осталось только реализовать Render(). По умолчанию, если не переопределить этот метод, вызывается метод Render() каждого элемента управления потомка. Однако, чтобы получить дополнительный контроль над этим, можно самим вызывать эти методы или, лучше, открытые экземпляры методов RenderControl():

protected override void Render(HtmlTextWriter writer) {

rainbowLabel.RenderControl(writer);

cycleButton.RenderControl(writer);

}

Здесь не выводится никакой код HTML, хотя можно было легко это сделать. Нам нужно просто передать полученный экземпляр HtmlTextWriter в метод RenderControl() для потомка, чтобы обычным образом был вставлен HTML, созданный этим потомком. Можно использовать такой элемент управления почти так же, как и RainbowLabel:

<form method="post" runat="server" ID="Form1">

<PCS:RainbowLabel2 Runat="server"

Text="Multicolored label composite" ID="rainbowLabel2" />

</form>

вместе со связанным объявлением в скрытом коде формы.

Элемент управления выборочным опросом

Теперь воспользуемся изложенной техникой и создадим более сложный специальный элемент управления. Конечный результат позволит с помощью следующего кода ASP.NET:

<form method="post" runat="server" ID="Form1">

<PCS: StrawPoll Runat="server" ID="strawPoll1"

PollStyle="voteonly" Title="Who is your favorite James Bond?">

<PCS:Option Name="SeanConnery" Votes="101" />

<PCS:Option Name="Roger Moore" Votes="83" />

<PCS:Option Name="George Lazenby" Votes="32" />

<PCS:Option Name="Timothy Dalton" Votes="28" />

<PCS:Option Name="Pierce Brosnan" Votes="95" />

</PCS:StrawPoll>

</form>

получить:

И когда мы нажмем на кнопку vote, изображение изменится на следующее:

Альтернативно, в основном для целей тестирования, можно выводить результаты и кнопки голосования одновременно, и разрешить подавать несколько голосов.

Код ASP.NET вовлечен явно в задание свойства Name и Votes для каждого варианта Option. Это прекрасно подходит для данного примера, хотя можно предвидеть, что более развитая версия этого элемента управления соединится с данными для получения результатов. Однако здесь это рассматриваться не будет, так как может оказаться достаточно сложным.

При анализе кода ASP.NET такие структуры интерпретируются согласованным образом: каждый управляющий элемент-потомок интерпретируется способом, который определен в классе-построителе элемента управления, связанным с элементом управления предком. Этот построитель элемента управления, код которого мы скоро увидим, обрабатывает все вложенное внутрь элемента управления, с которым он связан, включая литеральный текст.

Нам нужно создать два элемента управления: Option — для хранения отдельных вариантов выбора и StrawPoll, который будет содержать и выводить элемент управления выборочного опроса. Оба эти элемента управления будут помещены в новый файл исходного кода: StrawPoll.cs.

Элемент управления Option

Для начала создадим элементы управления Option, каждый из которых будет содержать имя варианта выбора и количество голосов, поданных за этот вариант выбора. Кроме того, эти элементы управления будут поддерживать кнопки голосования и обрабатывать все сделанные нажатия этих кнопок.

Поэтому нам потребуется:

□ Код для свойств Name и Votes (хранимых в ViewState)

□ Код инициализации в CreateChildControls()

□ Код для обработчика нажатия кнопки

Мы включаем также вспомогательный метод Increment(), который будет добавлять голос к текущему счету. Этот вспомогательный метод вызывается обработчиком нажатия кнопки.

Нам понадобится также поддержка INamingContainer, так как мы имеем несколько экземпляров этих элементов управления со своими собственными потомками.

Код класса Option будет находиться в файле StrawPoll.cs, который мы должны добавить к проекту вместе со стандартными инструкциями namespace и using, согласно уже известным нам элементам управления RainbowLabel. Код будет иметь следующий вид:

public class Option : System.Web.UI.WebControls.WebControl, INamingContainer {

public string Name {

get {

return (string)ViewState["_name"];

}

set {

ViewState["_name"] = value;

}

}

public long Votes {

get {

return (long)ViewState["_votes"];

}

set {

ViewState["_votes"] = value;

}

}

public void Increment() {

ViewState["_votes"] =(long)ViewState["_votes"] + 1;

}

public void Reset() {

ViewState["_votes"] = 0;

}

protected override void CreateChildControls() {

Button btnVote = new Button();

btnVote.Text = "Vote";

btnVote.Click += new System.EventHandler(btnVote_Click);

Controls.Add(btnVote);

}

protected void btnVote_Click(object sender, System.EventArgs e) {

Increment();

}

}

Отметим, что метод Render() не был здесь переопределен. Это связано с тем, что этот элемент управления имеет одного наследника, кнопку голосования и никакой другой информации для вывода. Поэтому можно использовать значение по умолчанию, которое будет просто изображением кнопки.

Построитель элемента управления StrawPoll

Теперь мы рассмотрим, как можно транслировать код ASP.NET каждого варианта выбора в элемент управления, который является потомком элемента управления StrawPoll. Чтобы сделать это, необходимо ассоциировать построитель элемента управления с классом StrawPoll с помощью атрибута ControlBuilderAttribute. Нам нужно также определить, что элементы управления-потомки не должны анализироваться никаким другим способом с помощью атрибута ParseChildren:

[ControlBuilderAttribute(typeof(StrawPollControlBuilder)) ]

[ ParseChildren(false) ]

public class StrawPoll : System.Web.UI.WebControls.WebControl, INamingContainer { }

Здесь используется класс с именем StrawPollControlBuilder, определенный следующим образом:

internal class StrawPollControlBuilder : ControlBuilder {

public override Type GetChildControlType(string tagName, IDictionary attribs) {

if (tagName.ToLower().EndsWith("option")) return typeof(Option);

return null;

}

public override void AppendLiteralString(string s) {

// ничего не делать, чтобы избежать добавления встроенного текста

// к элементу управления

}

}

Здесь мы переопределяем метод GetChildControlType() базового класса ControlBuilder чтобы он возвращал тип класса Option в ответ на тег с именем <Option>. Фактически, чтобы все работало в максимальном количестве ситуаций, мы ищем любое имя тега, которое оканчивается строкой "option" с буквами в верхнем или нижнем регистре.

Мы переопределяем также метод AppendLiteralString() так, чтобы любой промежуточный текст, включая пробелы, игнорировался и не вызывал никаких проблем.

Когда это сделано в предположении, что в StrawPoll нет никаких других элементов управления, мы будем иметь все элементы управления Option содержащимися в коллекции Controls из StrawPoll. Эта коллекция не будет содержать никаких других элементов управления.

Отметим, что построитель элементов управления использует коллекцию атрибутов. Чтобы использовать это добавим следующую инструкцию using в пространство имен:

using System.Collections;

Стиль StrawPoll

Прежде чем перейти к рассмотрению самого класса StrawPoll, необходимо рассмотреть еще один вопрос проектирования. StrawPoll должен выводиться в трех формах:

□ Только кнопки для голосования

□ Только результаты

□ Кнопки для голосования и результаты

Для этого можно определить перечисление, которое затем использовать как свойство элемента управления StrawPoll:

public enum pollStyle {

voteonly, valuesonly, voteandvalues

}

Как мы видели ранее, свойства, которые являются перечислениями, легко использовать, и мы можем применять текстовые имена в качестве значений атрибутов в ASP.NET.

Элемент управления StrawPoll

Теперь соберем все вместе. Для начала определим два свойства: Title дли вывода заголовка в элементе управления и PollStyle для хранения перечисления типа вывода. Оба они будут использовать ViewState для сохранения состояния:

[ ControlBuilderAttribute (typeof (StrawPollControlBuilder)) ]

[ ParseChildren(false) ]

public class StrawPoll : System.Web.UI.WebControls.WebContol, INamingContainer {

private string title = "Straw Poll";

private pollStyle currentPollStyle = pollStyle.voteandvalues;

public string Title {

get {

return title;

}

set {

title = value;

}

}

public pollStyle PollStyle {

get {

return currentPollStyle;

}

set {

currentPollStyle = value;

}

}

}

Остальная часть этого класса посвящена методу Render(). Он будет выводить весь элемент управления выборочного опроса вместе со всеми вариантами выбора, принимая в расчет используемый стиль опроса. Мы выводим кнопки голосования, вызывая метод RenderControl() производных элементов управления Option, и выводим результаты опроса графически и численно с помощью свойств Votes производных элементов управления Option для создания простого кода HTML.

Код, прокомментированный для ясности, будет выглядеть следующим образом:

protected override void Render(HtmlTextWriter writer) {

Option CurrentOption;

long iTotalVotes = 0;

long iPercentage = 0;

int iColumns = 2;

// Начало таблицы, изображение таблицы

if (currentPollStyle == pollStyle.voteandvalues) {

iColumns = 3;

}

writer.Write("<TABLE border='1' bordercolor='black' bgcolor='#DDDDEB'" +

" width= '90%' cellpadding='1' cellspacing='1'" + " align='center'>");

writer.Write("<TR><TD colspan='" + iColumns + align='center'"

+ " bgcolor='#FFFFDD'>");

writer.Write("<B>" + title + "</B></TD></TR>");

if (Controls.Count == 0) {

// текст по умолчанию, когда нет вариантов выбора

writer.Write("<TR><TD bgcoLor='#FFFFDD'>No options to" + " display.</TR></TD>");

} else {

// Получить общее число голосов

for (int iLoop = 0; iLoop < Controls.Count; iLoop++) {

// Получить вариант выбора

currentOption = (Option)Controls[iLoop];

// Просуммировать результаты голосования

iTotalVotes += currentOption.Votes;

}

// Вывести каждый вариант выбора

for (int iLoop = 0; iLoop < Controls.Count; iLoop++) {

// Получить вариант выбора

currentOption = (Option)Controls[iLoop];

// Поместить имя варианта выбора в первый столбец

writer.Write("<TR><TD bgcolor='#FFFFDD' width="15%'> " +

currentOption.Name + " </TD>");

// Добавить вариант голосования во второй столбец,

// если требуется

if (currentPollStyle != pollStyle.valuesonly) {

writer.Write("<TD width='1%' bgcolor='#FFFFDD'>"

+ "<FONT Color='#FFFVDD'>.</FONT>");

currentOption.RenderControl(writer);

writer.Write("<FONT Color = '#FFFFDD'>.</FONT></TD>");

}

// Поместить график, значение и проценты в третьем столбце,

// если требуется

if (currentPollStyle != pollStyle.voteonly) {

if (iTotalVotes > 0) {

iPercentage = (currentOption.Votes * 100) / iTotalVotes;

} else {

iPercentage = 0;

}

writer.Write("<ТD bgcolor='#FFFFDD'><TABLE width='100%'>"

+ "<TR><TD><TABLE border='1' bordercolor= 'black' "

+ " width= '100%' cellpadding='0' " + " cellspacing='0'>");

writer.Write("<TR><TD bgcolor='red' width='" + iPercentage

+ "%'><FONT соlor='red'>.</FONT></TD>");

writer.Write<"TD bgcolor='white' width='" + (100-iPercentage) +

"%'><FONT color='white'>." +

"</FONT></TD></TR></TABLE></TD>");

writer.Write("<TD width='75'>" + сurrentOption.Votes +

" (" + iPercentage + "%)</TD><TR></TABLE></TD>");

}

// Конец строки

writer.Write("</TR>");

}

// показать общее тело голосов, если выводятся значения

if (currentPollStyle != pollStyle.voteonly) {

writer.Write("<TR><TD bgcolor='#FFFFDD' colspan='" +

iColumns + "'>Total votes cast: " + iTotalVotes + "</TD></TR>");

}

}

// Завершить таблицу

writer.Write("</TABLE>");

}

Если выборочный опрос выводится в режиме voteonly, то голосование должно инициировать изменение изображения в режиме valuesonly. Чтобы сделать это, нам потребуется небольшое изменение в обработчике кнопки голосования в классе Option:

protected void btnVote_Click(object sender, System.EventArgs e) {

Increment();

StrawPoll parent = (StrawPoll)Parent;

if (parent.PollStyle == pollStyle.voteonly) {

parent.PollStyle = pollStyle.valuesonly;

}

}

Теперь все готово к проведению голосования.

Добавление обработчика событий

Часто при использовании специальных элементов управления желательно инициировать специальные события и предоставить пользователям элемента управления действовать в соответствии с этим. В случае элемента управления выборочного опроса было бы хорошо иметь событие Voted, которое будет уведомлять форму, что голосование выполнено, и предоставлять ей всю информацию, необходимую для действия в этом случае.

Чтобы зарегистрировать специальное событие, надо добавить в элемент управления код, аналогичный представленному ниже:

public event EventHandler Voted;

protected void OnVoted(EventArgs e) {

Voted(this, e);

}

Тогда, как только нам понадобиться инициировать событие, мы просто вызываем метод OnVoted(), передавая аргументы события.

Когда вызывается OnVoted(), инициируется событие, в соответствии с которым может действовать пользователь. Чтобы сделать это, пользователю необходимо зарегистрировать обработчик событий для этого события:

strawPoll1.Voted += new EventHandler(this.StrawPoll1_OnVoted);

Пользователь должен также предоставить код обработчика strawPoll1_OnVoted(). Мы слегка расширим этот метод, добавляя специальные аргументы для события, чтобы сделать доступным элемент управления Option, который инициирует событие. Назовем наш объект специального аргумента OptionEventArgs и определим его в StrawPoll.cs следующим образом:

public class OptionEventArgs : EventArgs {

public Option originatingOption;

}

Добавляем дополнительное открытое поле в существующий класс EventArgs. Так как мы изменили используемые аргументы, нам потребуется также специализированная версия представителя EventHandler, которая может объявляться в пространстве имен PCSCustomWebControls следующим образом:

public delegate void Option EventHandler(object sender, OptionEventArgs e);

Можно использовать эти примеры в StrawPoll следующим образом:

public class StrawPoll : System.Web.UI.WebControls.WebControl, INamingContainer {

private string title = "Straw Poll";

private pollStyle currentPollStyle = pollStyle.voteandvalues;

public event OptionEventHandler Voted;

protected void OnVoted(OptionEventArgs e) {

Voted(this, e);

}

Также мы имеем метод для инициирования события, вызываемый из элементов управления потомков Option при нажатии кнопки голосования:

public void ChildVote(OptionEventArgs e) {

OnVoted(e);

}

Наконец, нужно сделать дальнейшую модификацию обработчика события кнопки голосования в Option, чтобы вызывать этот метод, задавая для него правильные параметры:

protected void btnVote_Click(object sender, System.EventArgs e) {

Increment();

StrawPoll parent = (StrawPoll)Parent;

if (parent.PollStyle == pollStyle.voteonly) {

parent.PollStyle = pollStyle.valuesonly;

}

OptionEventArgs eOption = new OptionEventArgs();

eOption.originatingOption = this;

parent.ChildVote(eOption);

}

Теперь можно реализовать обработчик событий на странице с помощью элемента управления. Мы должны просто определить его на странице ASP.NET, добавляя метку для использования в обработчике.

<form id=Form1 method= post runat="server">

<PCS:StrawPoll id=strawPoll1

title="Who is your favorite James Bond?"

Runat="server" OnVoted="strawPoll1_OnVoted" PollStyle="voteonly">

<PCS:Option Name="Sean Connery" Votes="101" />

<PCS:Option Name="Roger Moore" Votes="83" />

<PCS:Option Name="George Lazenby" Votes="32" />

<PCS:Option Name="Timothy Dalton" Votes="28" />

<PCS:Option Name="Pierce Brosnan" Votes="95" />

</PCS:StrawPoll>

<br> <br>

<asp:Label Runat= "server" ID="resultLabel" Text="No vote cast." />

</form>

вместе со связанным объявлением в скрытом коде формы, если он не добавляется автоматически:

public class WebForm1 : System.Web.UI. Page {

protected StrawPoll strawPoll1;

Затем сделаем что-нибудь в самом обработчике событий:

protected void strawPoll1_OnVoted(object sender, OptionEventArgs e) {

result.Label.Text = "You voted for " + e.originatingOption.Name + ".";

}

Теперь, после голосования мы получаем ответную реакцию на наш голос:

Заключение

В этой главе были рассмотрены различные способы создания повторно используемых серверных элементов управления ASP.NET с помощью C#. Мы увидели, как создаются простые элементы управления пользователя из существующих страниц ASP.NET, а также специальные элементы.

С помощью специальных элементов управления можно сделать много разных вещей, но, к сожалению, невозможно рассмотреть все это в одной главе. Однако здесь представлено достаточно информации, чтобы можно было начать. В частности, было бы интересно посмотреть на связывание данных и на то, как можно создавать свои собственные элементы, использующие эту возможность. Также было бы полезно рассмотреть более подробно некоторые области (например, расширить рассмотрение свойств и конструкторов элементов управления). Хотя такие вопросы лучше подходят для более специализированных книг.

Эта глава завершает три главы, посвященные Web. Далее мы перейдем к рассмотрению взаимодействия COM и .NET.

Глава 19

Взаимодействие с COM

Компоненты COM и компоненты .NET не являются по своей природе совместимыми, так как они опираются на различные внутренние архитектуры. К счастью, однако, компания Microsoft предоставляет средства в SDK .NET для создания прокси COM для компонентов .NET и прокси .NET для компонентов COM. Используя эти прокси вместе с парой других технологий, организации могут использовать унаследованные компоненты COM в своих проектах .NET и могут также использовать компоненты .NET в своих приложениях, не являющихся приложениями .NET.

В этой главе будет показано, как достичь взаимодействия между COM и .NET, что поможет понять, почему COM и .NET будут играть различные, но жизненно важные роли в будущих приложениях Windows.

Сравнение COM и .NET

COM обозначает "Компонентная объектная модель" (Component Object Model). Для новичков платформы Windows понимание того, что делает COM, может быть трудным, а понимание того, как она действует, может показаться почти невозможным. Для новичков, не интересующихся созданием драйверов устройств или работой на компанию Microsoft, появление .NET и компонентов .NET позволит избежать сложностей программирования COM. Однако, так как COM была до сегодняшнего дня все же существенной частью программирования Windows, желательно быть знакомым с тем, как она действует и какие преимущества она предоставляет.

Повторное использование кода является одной из священных идей разработки программного обеспечения, и ранее разработчики добивались этого, поддерживая файлы стандартных функций. Когда разработчик хотел использовать стандартную функцию в одной из своих программ, он использовал инструкцию #include для соединения библиотечного файла и файла основной программы. После компиляции стандартные функции и программа, которая их вызывала, статически компоновались в один исполнимый файл.

Существует, по крайней мере, два недостатка у статической компоновки: она требует излишнего пространства памяти, размещая избыточные копии идентичных функций в нескольких исполнимых файлах, и, если обнаруживается ошибка в одной из стандартных функций, требуется перекомпиляция и повторное распространение всех исполнимых файлов, использующих эту функцию. Чтобы избежать таких проблем, разработчики нашли способ компилировать библиотеки функций в автономные двоичные файлы, которые могут динамически компоноваться различными исполнимыми файлами. С помощью этой схемы несколько исполнимых программ могут совместно использовать один и тот же двоичный файл динамически компонуемой библиотеки (Dynamic Link Library, DLL). Если требуется исправить ошибку в служебной функции, можно просто распространить заново DLL, в которой находится эта функция, без перекомпиляции или повторного распространения всех исполнимых файлов, которые ее используют.

Несмотря на свое удобство и эффективность, DLL имеют один серьезный недостаток: они зависят от конкретного языка программирования. Другими словами, DLL, компилированная из исходного кода С, может использоваться только из клиентской программы, которая также была написана на С. (Существуют исключения из этого правила, но такие исключения требуют от программиста специальных усилий).

COM была следующим шагом в эволюции повторного использования кода. С ее помощью программисты могли написать библиотеку классов на таком языке, как C++, откомпилировать библиотеку и использовать классы этой библиотеки из другого совместимого с COM языка программирования, такого как Delphi или Visual Basic. COM был технологией, лежащей в основе других технологий, таких как OLE (Связывание и встраивание объектов) и элементов управления ActiveX.

Службы COM+ — последняя версия технологии COM, первоначально известная как Сервер транзакций Microsoft, является частью операционной системы Windows 2000, которую компоненты COM могут использовать для общей, необходимой компонентам, функциональности: поддержки транзакций, обеспечения безопасности и события, что позволит сохранить программистам COM ценное время разработки. Более подробно об этом рассказывается в следующей главе.

Принципы работы COM

Чтобы понять, почему компоненты COM и компоненты .NET внутренне несовместимы, необходимо иметь общее понимание того, как работает COM. Далее следует существенно упрощенное объяснение. Более детальную информацию можно найти в книге "Professional COM Applications with ATL" издательства Wrox Press (ISBN 1861001703).

COM устанавливает стандарты для интерфейсов, с помощью которых клиентский код общается с массами компонентов. Так как клиентский код общается с классами компонентов только через эти стандартизованные интерфейсы, он может оставаться полностью несведущим о деталях, специфических для языка программирования, то есть о том, как реализованы эти классы компонентов. Например, используя интерфейсы COM в качестве промежуточных, клиент VB может ссылаться на компоненты COM, первоначально закодированные на C++.

Интерфейсы COM предоставляют другие возможности в дополнение к межъязыковой коммуникации. Интерфейс COM IUnknown, например, позволяет объекту COM подсчитать число клиентов, которые на него ссылаются, и автоматически выгрузить себя из памяти, когда этот счетчик уменьшится до нуля. Более того, реализуя интерфейсы, распознаваемые службами COM+, класс COM может воспользоваться написанной ранее функциональностью для безопасности, организации пула объектов, и сохранения объектов.

Каждый класс COM и каждый интерфейс, который он поддерживает, имеет 128-битный идентификатор, который гарантированно является уникальным во все времена и во всех местах. Эти глобально уникальные идентификаторы, или GUID, централизованно хранятся в реестре машины, и поддерживающие COM языки имеют средства для получения значения GUID компонента COM и использования его для вызова функциональности компонента.

Недостатки COM

Хотя COM предоставляет значительные преимущества, она имеет также недостатки. Первое: компоненты COM могут быть трудными для кодирования. В C++ разработка компонента COM включает реализацию стандартных интерфейсов COM и использование GUIDGEN.EXE для того, чтобы генерировать GUID для каждого класса и каждого интерфейса. (Хотя технологии, подобные VB и ATL Object Wizard упрощают процесс создания COM, они обеспечивают только подмножество свойств COM).

Второе: компоненты COM могут оказаться трудными для развертывания. Разработчики COM серверных компонентов предполагали обеспечить совместимость новых версий компонентов с более старыми, но не всегда это удавалось, поэтому установка нового приложения, которое ссылается на новую версию компонента COM, может внезапно дать отказ существующих приложений. Проблемы такого рода называются "адом DLL" и являются причиной большой головной боли и потерь времени.

Дополнительная информация о том, как .NET обращается с адом DLL, находится по адресу msdn.microsoft.com/library/techart/dplywithnet.htm.

Как работают компоненты .NET

Подход .NET к созданию компонентов использует многие возможности COM, игнорируя при этом ее недостатки. Компоненты должны иметь некоторый способ описания клиентам классов, которые они поддерживают. Вместо использования для этого GUID и реестра каждый файл компонента .NET инкапсулирует свое собственное описание во внутреннем сегменте, называемом манифестом (manifest).

Это означает, что размещение компонента выполняется просто: надо скопировать компонент .NET в папку исполнимого файла, который на него ссылается. Когда исполнимому файлу необходимо создать компонент, он просматривает в файле компонента в манифесте информацию, которая ему нужна. Различные версии одного и того же компонента могут существовать бок о бок на одной машине до тех пор, пока они хранятся в различных папках. (Существует центральная папка для хранения компонентов, которую можно сделать доступной для множества компонентов, она называется иногда "Global Assembly Cache" — Глобальный кэш сборок (см. главу 8).

Компоненты .NET также легко создавать в C# и VB.NET разработчик защищен от процесса создания манифеста. Разработчик создает просто проект библиотеки классов, заполняет его классами и позволяет компилятору выполнить грязную работу предоставления этих классов клиентам.

COM или .NET?

.NET не исключает COM. Многие организации, включая Microsoft, вложили значительные ресурсы в разработку COM. К тому же, поскольку компоненты .NET интерпретируются и используют среду времени выполнения .NET, они не так хорошо подходят, как компоненты COM, для сред выполнения, где скорость и эффективность являются основными требованиями.

Однако для разработки на уровне предприятия может оказаться, что компоненты .NET окажутся более практичными, чем компоненты COM. В организациях, которые действуют со скоростью Интернет, достаточно хорошее программное обеспечение, сделанное вовремя, предпочтительнее оптимизированного программного обеспечения, которое опоздало на шесть месяцев. Если программное обеспечение запускается с сервера Web, то можно гарантировать, что среда выполнения .NET правильно сконфигурирована, и таким образом получается выигрыш от сохраненного времени разработки и развертывания, что обеспечивает платформа .NET.

Речь не идет о том, что компоненты COM лучше, чем сборки .NET, или что сборки .NET лучше, чем компоненты COM. Сборки .NET, хотя и требуют больше поддержки во время выполнения, делают компонентную архитектуру более доступной для программистов.

Использование компонентов COM в .NET

В типичной организации, вероятно, невозможно (или, по крайней мере, нежелательно) выбросить компоненты COM, которые были в ней разработаны, так как компоненты .NET сделаны хорошо. Следовательно, можно ожидать ссылок на унаследованные компоненты COM из нового кода .NET, по крайней мере, в первых проектах .NET масштаба предприятия. В частности, если создается новое приложение .NET поверх существующей базы данных, вероятно, захочется использовать существующие объекты COM доступа к данным в слое доступа к данным создаваемого проекта. Если доступ к данным происходит через унаследованные компоненты, то .NET реализует бизнес-правила и доставит данные в формы ASP.NET или Windows интерфейса пользователя.

Взаимодействие COM использует классы "оболочки" и компоненты "прокси" — обычные соглашения в мире программирования в целом. Класс оболочки окружает класс, соответствующий другой архитектуре, предоставляя к нему знакомый интерфейс клиентам, которые не будут распознавать собственный интерфейс завернутого класса. Аналогично, клиент может использовать компонент прокси для доступа к компоненту, который соответствует другой архитектуре или является географически удаленным.

Диалоговое окно ссылок

Прежде чем погружаться в тонкости вопросов взаимодействия с унаследованными объектами COM, давайте кратко обсудим, как достичь такого взаимодействия. Наше объяснение строится с точки зрения IDE VS.NET, так как большинство читателей будут использовать его для написания своих программ C#, но альтернативные редакторы могут предоставить свои собственные эквивалентные методы.

Для этого понадобиться использовать диалоговое окно ссылок (References Dialog), которое доступно из пункта меню Add Reference… в меню Project IDE Visual Studio.NET. Это диалоговое окно имеет три вкладки, первая из которых просто перечисляет DLL, которые являются вспомогательными, но при этом очень важными для среды времени выполнения платформы .NET.

Самая правая вкладка, Projects, перечисляет все проекты .NET, на которые ссылаются в данном решении. На следующем экране, например, клиентское приложение C# для онлайновых заказов опирается на несколько проектов библиотек классов C#, которые реализуют бизнес-правила. (Конечно, они не обязаны быть проектами C#, чтобы на них ссылался клиент C#, они могут также легко быть проектами VB.NET. Фактически, один клиент .NET может одновременно ссылаться на проекты компонентов на нескольких различных языках платформы .NET).

Средняя вкладка, COM, является вкладкой для импортирования компонентов COM в проекте .NET.

В верхнем правом углу диалогового окна имеется кнопка Browse. При нажатии на эту кнопку появится другое диалоговое окно, которое позволит найти в файловой системе DLL COM, требующиеся проекту .NET:

Когда файл будет найден, добавляем его в список компонентов в панели формы COM:

После использования диалогового окна ссылок для нахождения DLL COM и добавления библиотеки к списку ссылок COM, можно использовать компонент COM в коде .NET. VS.NET создает пространство имен с таким же именем, как и у исходного компонента COM, и классы этого компонента COM помещаются в это пространство имен. Ссылки, создание экземпляров и вызов оболочки объекта COM производятся с таким же синтаксисом, как и у собственных объектов C#.

Посмотрим на следующий пример кода. В нем определен метод для добавления нового заказчика в базу данных. В качестве аргумента ввода этот метод получает ссылку на объект CustomerInfo, поля которого содержат имя определенного заказчика, код социального обеспечения, и т.д. Экземпляр класса доступа к данным CustomerTable создается из компонента COM и используется для вставки информации о заказчике в базу данных. В этом примере важно то, что код, связанный с объектом COM, является кодом обыкновенного объекта C#. Мы создаем экземпляр оболочки .NET и позволяем ему делегировать свою работу реальному объекту COM за сценой:

/// <summary>

/// Этот код добавляет нового заказчика в базу данных,

/// обеспечивая при этом выполнение бизнес-правил.

/// </summary>

public long AddNewCustomer(CustomerInfo objCustomerInfo) {

long lngNewCustomerID;

DataAccess.CustomerTable objCustomerTable;

// Добавить запись в таблицу заказчиков.

objCustomerTable = new DataAccess.CustomerTable();

lngNewCustomerID =

objCustomerTable.InsertRecord(

objCustomerInfo.LastName, objCustomerInfо.FirstName,

objCustomerInfo.MiddleName, objCustomerInfo.SocialSecurityNumber);

Конечно, если надоест вводить пространство имен компонента COM, то инструкция using в начале каждого файла позволит сослаться на классы в оболочке компонента COM с помощью сокращенных, относительных имен:

// Размещение следующей инструкции в начале файла ...

using DataAccess;

// позволяет ссылаться на классы DataAccess по их относительным именам

CustomerTable objCustomerTable;

objCustomerTable = new CustomerTable();

IDE VS.NET будет даже использовать Intellisense, чтобы помочь запомнить членов класса компонента:

Intellisense поможет также с членами данных и списком аргументов.

Оболочки времени выполнения

Чтобы добавить ссылку на DLL COM при использовании диалогового окна References, IDE VS.NET делает некоторую работу за сценой. В частности, создается компонент прокси .NET для DLL COM и копия DLL COM помещается в каталог проекта .NET.

Вспомните, что компоненты .NET описывают сами себя, в то время как компоненты COM хранят свои описания в реестре. Прокси, который генерирует IDE VS.NET, описывает DLL COM и служит для нее в качестве делегата, пересылая вызовы от клиента .NET через службы COM в DLL COM, которую он обертывает. Клиент .NET не знает, что он вызывает компонент COM; он общается только с прокси и получает данные, которые прокси пересылает обратно из DLL COM.

В терминологии .NET такой прокси называется оболочкой времени выполнения, или RCW. IDE создает DLL, которая имеет такое же имя, как и исходный компонент COM, но является в действительности .NET RCW, которая просто обертывает первоначальный компонент, предоставляя его клиентам .NET через интерфейс .NET, который они могут понять. Интересное дополнительное замечание: IDE VS.NET создает оболочку не только для каждой импортируемой DLL COM, но для каждой DLL COM, на которую ссылается импортированная DLL COM в своем открытом интерфейсе, поэтому в этой папке будет также находиться файл ADODB.dll, на который в DataAccess.dll существует ссылка.

В рассматриваемом примере DLL COM, DataAccess.dll предоставляет методы для вставки, извлечения, обновления и удаления записей в нескольких таблицах базы данных. Так как методы вставки возвращают множества записей ADO, клиентам компонента COM требуется ссылка на библиотеку типов ADO. При импорте DataAccess.dll среда разработки распознает эту необходимость и автоматически создает также RCW для компонента COM ADO.

TlbImp.exe

Опасно ли для RCW, созданной IDE VS.NET, иметь такое же имя, как и исходная DLL COM? В конце концов, если приложение переносится с одной машины на другую, будет очень легко спутать RCW с DLL COM, которую он заворачивает, и случайно перезаписать файл DLL COM. Или можно по ошибке попытаться зарегистрировать RCW в службах COM, удивляясь, почему программа регистрации (regsvr32.exe) не работает.

Чтобы избежать таких проблем, попробуйте воспользоваться напрямую утилитой TlbImp.exe. Поставляемая вместе с SDK .NET эта исполняемая программа специализируется на создании прокси .NET для DLL COM. Так как она вызывается из командной строки, можно задать TlbImp.exe с аргументом командной строки out, чтобы получающийся RCW имел имя, отличное от имени DLL COM.

TlbImp является сокращением от Type Library Importer (Импортер библиотеки типов). При выполнении этой программы с DLL COM она запрашивает библиотеку типов DLL COM и транслирует информацию оттуда в формат .NET, преобразуя стандартные типы данных COM в типы, распознаваемые .NET. После выполнения TlbImp.exe для DLL COM, файл вывода (RCW) просто помещается в папку исполняемого файла клиента, который будет его использовать. (Это дополнительный шаг, который необходим при явном использовании TlbImp.exe вместо диалогового окна References в IDE.) В приведенном выше примере TlbImp.exe выполняется с DLL COM, которая находится в той же папке, что и TlbImp, но TlbImp.exe работает, как любая другая утилита командной строки, т.е. можно определить файл в другой папке с помощью абсолютного или относительного пути доступа.

В заключение пара предупреждении о TlbImp.exe и RCW. Первое: не забывайте задавать аргумент out при использовании TlbImp.exe. Если не сделать этого, то программа TlbImp.exe будет заявлять, что она не может перезаписать исходный файл:

Второе: помните, что хотя RCW служит в качестве посредника между компонентом COM и клиентом .NET, который его вызывает, по-прежнему всю реальную работу делает компонент COM. То есть необходимо выполнить те же требования к развертыванию компонента COM, которые пришлось бы сделать, если бы он использовался напрямую. Это означает, что завернутый компонент COM тем не менее надо регистрировать в службах COM. Если попробовать сослаться на незарегистрированный компонент COM, то IDE VS.NET будет порождать ошибку.

Чтобы справиться с этой проблемой, понадобится, конечно, программа регистрации COM — regsvr32.exe. Ее можно вызвать из диалогового окна Windows Run, которое доступно из меню Start рабочего стола.

Поэтому не забывайте регистрировать компоненты COM.

Позднее связывание с компонентами COM

Прежде чем исполнимый файл клиента сможет вызвать методы и свойства объекта компонента, ему необходимо узнать адреса памяти этих методов и свойств. Существуют две различные технологии, которые клиентские программы могут использовать для определения этих адресов.

Программы с ранним связыванием узнают адреса на ранней стадии процесса компиляции/выполнения — во время компиляции. Когда программа (ранним связыванием компилируется, компилятор использует библиотеку типов компонента для включения адресов методов и свойств компонента в клиентскую исполнимую программу, чтобы к адресам можно было очень быстро и безошибочно обращаться. Технологии взаимодействия COM которые были рассмотрены до сих пор, используют раннее связывание.

Со своей стороны, программы с поздним связыванием узнают адреса свойств и методов на поздней стадии процесса компиляции/выполнения, в тот самый момент, когда эти свойства и методы вызываются. Код с поздним связыванием обычно обращается к клиентским объектам через базовые типы данных, такие как object, и использует среду времени выполнения для динамического определения адресов методов. Хотя код с поздним связыванием позволяет использовать некоторые сложные технологии программирования, такие как полиморфизм, он требует некоторых связанных расходов, которые мы вскоре увидим.

Но сначала проверим, как позднее связывание выполняется с помощью отражения в C# (Отражение, является способом, который используется кодом во время выполнения для определения информации об интерфейсах серверных классов; см. главу 5.)

При позднем связывании с объектом COM в программе C# не нужно создавать RCW для компонента COM. Вместо этого вызывается метод класса GetTypeFromProgID класса Type для создания экземпляра объекта, представляющего тип объекта COM. Класс Type является членом пространства имен System.Runtime.InteropServices и в коде ниже мы конфигурируем объект Type для того же компонента COM доступа к данным, который использовался в предыдущих примерах:

using System.Runtime.InteropServices;

Type objCustomerTableType;

objCustomerTableType = Type.GetTypeFromProgID("DataAccess.CustomerTable");

Когда имеется объект Type, инкапсулирующий информацию о типе объекта COM, он используется для создания экземпляра самого объекта COM. Это реализуется передачей объекта Type в метод класса CreateInstance класса Activator.CreateInstance создает экземпляр объекта COM и возвращает на него ссылку позднего связывания, которую можно сохранить в ссылке типа object.

object objCustomerTable;

objCustomerTable = Activator.CreateInstance(objCustomerTableType);

В этом месте код C# имеет ссылку позднего связывания на готовый экземпляр класса COM.

К сожалению, невозможно вызывать методы непосредственно на ссылке типа object. Чтобы можно было обратиться к объекту COM, необходимо использовать метод InvokeMember объекта Type, который был создан вначале. При вызове метода InvokeMember ему передается ссылка на объект COM вместе с именем вызываемого метода COM, а также массив типа object всех входящих аргументов метода.

ObjCustomerTableType.InvokeMember("Delete", BindingFlags.InvokeMethod, null, objCustomerTable, aryInputArgs);

Напомним еще раз последовательность действий:

1. Создать объект Type для типа объекта COM с помощью метода класса Type.GetTypeFromProgID().

2. Использовать этот объект Type для создания объекта COM с помощью Activator.CreateInstance().

3. Методы вызываются на объекте COM, вызывая метод InvokeMember на объекте Type и передавая в него ссылку object в качестве входящего аргумента. Ниже приведен пример кода, объединяющий все это в один блок:

using System.Runtime.InteropServices;

Type objCustomerTableType;

object objCustomerTable;

objCustomerTableType=Type.GetTypeFromProgID("DataAccess.CustomerTable");

objCustomerTable=Activator.CreateInstance(ObjCustomerTableType);

objCustomerTableType.InvokeMember("Delete", BindingFlags, InvokeMethod, null, objCustomerTable, aryInputArgs);

objCustomerTableType = Type.GetTypeFromProgID("DataAccess.CustomerTable");

Хотя средства позднего связывания C# позволяют избежать трудностей RCW, необходимо знать о некоторых, связанных с этим, недостатках.

Первый: позднее связывание может быть опасным. При использовании раннего связывания компилятор может запросить библиотеку типов компонента COM, чтобы убедиться, что все вызываемые на объектах COM методы в действительности существуют. При позднем связывании ничто не препятствует опечатке в вызове метода InvokeMember(), что может породить ошибку во время выполнения.

Второй: позднее связывание может быть медленным. Каждый раз при использовании InvokeMember() на объектной ссылке среда времени выполнения должна найти требуемый член в библиотеке функций класса COM. Это приводит к снижению производительности программы.

Третий: написание кода с поздним связыванием может оказаться трудоемким. Так как не требуется ссылаться на библиотеку типов компонента COM, IDE VS.NET не может использовать Intellisense, чтобы помочь с именами членов и списками аргументов, поэтому в коде могут появиться ошибки, которые будет трудно найти до времени выполнения.

Использование элементов управления ActiveX в .NET

Элемент управления ActiveX является частным типом компонента COM, который поддерживает специальное множество интерфейсов для обеспечения графического представления. Также, как можно импортировать стандартные компоненты COM для использования в проектах .NET, можно также импортировать элементы управления ActiveX. Это позволяет сделать утилита AxImp.exe.

AxImp.exe

Чтобы импортировать компонент ActiveX в среду .NET, утилита AxImp.exe вызывается из командной строки. Команда состоит из двух частей:

1. Имени AxImp.

2. Абсолютного или относительного пути доступа к файлу ActiveX (*.осх), который должен быть импортирован.

Для примера рассмотрим снимок экрана, приведенный ниже. Здесь импортируется элемент управления ActiveX Win32 MAPI и определяется расположение файла .осх (C:\windows\system\msmapi32.осх):

Как можно видеть, программа AxImp.exe выводит два файла.

Первый выходной файл MSMAPI.dll является прокси для сборки. Он позволяет ссылаться на компонент ActiveX, как если бы это был неграфический объект, с помощью его методов и свойств таким же образом, каким используются методы и свойства неграфического класса.

Второй файл AxMSMAPI.dll является элементом управления Windows. Он позволяет использовать графический аспект импортированного элемента управления ActiveX как элемент управления Windows в проектах Forms Windows .NET.

При использовании AxImp желательно убедиться, что для этих двух выходных файлов предоставляются явные и уникальные имена, так как, по крайней мере, в бета-версии SDK .NET AxImp.exe имеет потенциальную возможность перезаписывать входные файлы без предупреждения.

Ссылка на сборку прокси ActiveX

Как сказано выше, вывод сборки прокси утилитой AxImp.exe позволяет ссылаться на компонент ActiveX программным путем без использования графических аспектов. Для этого необходимо просто добавить ссылку на прокси сборки с помощью вкладки Reference в IDE VS.NET после того, как AxImp.exe сделает свою работу:

После создания ссылки на сборку прокси можно использовать в коде программы компонент ActiveX. В примере ниже используется прокси MSMAPI32.осх для отправки почтового сообщения:

public static int Main() {

MAPISession objSession=new MAPISession();

MAPIMessages objMessage=new MAPIMessages();

objSession.Password="password";

objSession.UserName="me@mydomain.com";

objSession.SignOn();

objMessage.SessionID=objSession.SessionID;

objMessage.Compose();

objMessage.MsgSubject="MAPI Contol Test";

objMessage.MsgNoteText="The message body.";

objMessage.RecipAddress="you@yourdomain.com";

objMessage.ResolveName();

objMessage.Send(null);

objSession.SignOff();

return 0;

}

Размещение элемента управления ActiveX в WinForm

Также достаточно просто разместить элемент управления ActiveX в форме Windows. Чтобы сделать это, сначала запустим диалоговое окно Customize Toolbox из IDE VS.NET, щелкнув правой кнопкой мыши в панели инструментов IDE и выбрав соответствующий пункт в контекстном меню. Когда появится диалоговое окно, надо перейти на вкладку .NET Framework Components и найти созданный утилитой AxImp.exe файл элемента управления Windows. После закрытия диалогового окна Customize Toolbox импортированный элемент управления появится в панели управления IDE и можно будет добавить его в Windows Form таким же образом, каким добавляются любые другие элементы управления Windows.

Использование компонентов .NET в COM

Также, как используются компоненты COM и элементы управления ActiveX в коде .NET, можно применять компоненты .NET в стандартном коде Windows. Только несколько свойств сборок .NET недоступны через COM, включая параметризованные конструкторы, статические методы и константные поля. Кроме того, доступ к перегруженным методам .NET из COM требует дополнительной работы.

RegAsm.exe

Применение компонентов COM в коде .NET требует другой утилиты, которая существует в SDK .NET, являющейся аналогом программы импорта библиотеки типов, которую мы видели ранее. Название этой утилиты — RegAsm.exe.

Название утилиты RegAsm (Register Assembly) обозначает ее функцию, она отвечает за ввод информации о типе компонента .NET в системный реестр, чтобы службы COM могли к нему обратиться. После регистрации компонента .NET с помощью RegAsm, стандартные клиенты Windows могут сделать позднее соединение с классами компонента. Процесс регистрации компонента должен быть сделан только один раз. После регистрации все клиенты COM могут к нему обращаться.

В качестве примера рассмотрим следующий код. Он принадлежит классу в библиотеке классов .NET. Функция получает просто число в качестве аргумента ввода и возвращает факториал этого числа:

namespace Factorial {

using System;

public class Factorial {

// Этот метод вычисляет факториал числа

public int ComputeFactorial(int n) {

int intFactorial=n;

for (int i = 1; i<n; i++) {

intFactorial*=i;

}

return intFactorial;

}

}

}

После компиляции класса примера в сборку .NET можно зарегистрировать эту сборку в службах COM с помощью RegAsm.exe:

Теперь, когда сборка зарегистрирована в службах COM с помощью RegAsm, можно выполнить позднее связывание со сборкой .NET через службы COM. С целью демонстрации создадим простой сценарий VB, который это делает. (Сценарий VB можно создать с помощью текстового редактора, такого как Notepad; введите просто следующий код и сохраните файл с расширением .vbs. Предполагая, что Windows Script Host установлен, файл при вызове будет выполняться как сценарий. Помните, что сценарий VB выполняет позднее связывание для компонентов COM.)

Option Explicit

Dim objFactorial

Dim lngResult

Dim lngInputValue

Set objFactorial=CreateObject("Factorial.Factorial")

lngInputValue=InputBox("Numbers?")

IngResult=objFactorial.ComputeFactorial(CLng(lngInputValue))

Call MsgBox(lngResult)

Однако, прежде чем это можно будет использовать, необходимо установить сборку в глобальный кэш. Для этого сначала создадим устойчивое имя сборки с помощью следующей команды:

sn -k Factorial.dll

Далее необходимо создать файл AssemblyInfo.cs со следующим содержимым:

using System.Reflection;

[assembly: AssemblyKeyFile("factorial.snk")]

Затем это надо откомпилировать с помощью следующей команды, чтобы превратить в модуль:

CSC /t:module /out: AssemblyInfo.dll AssemblyInfo.cs

После этого компилируется файл Factorial.cs, и полученная DLL устанавливается в глобальный кэш с помощью gacutil следующим образом:

csc /t:library /addmodule:assemblyinfо.dll Factorial.cs gacutil /i Factorial.dll

При выполнении сценарий VB воспользуется службами COM для создания объекта .NET, вызовет метод на этом объекте и выведет возвращаемое из объекта .NET значение в окне сообщения:

Интересная техника, не правда ли? Но она не решает ни одной из упомянутых выше проблем, связанных с поздним связыванием. К счастью, другой член набора инструментов SDK .NET может в этом помочь. Это утилита TlbExp.exe.

Однако, прежде чем попрощаться с TlbImp.exe, необходимо запомнить одну вещь: службы COM должны иметь возможность найти компонент сборки .NET, когда он вызывается. Это означает, что сборка должна располагаться в рабочей папке клиента или в глобальном коде сборки системы.

TlbExp.exe

TlbExp обозначает Type Library Exporter (экспортер библиотеки типов). При выполнении на файле сборки .NET TlbExp может запросить внутренний манифест этой сборки и вывести соответствующий файл библиотеки типов COM (*.tlb). Когда TlbExp создаст файл библиотеки типов для компонента .NET, языки разработки не принадлежащие .NET, такие как VB6, смогут ссылаться на него, используя для того, чтобы получить эффективное раннее связывание с компонентами .NET:

Как можно было уже догадаться, утилиты TlbExp и RegAsm были созданы для работы в тесном взаимодействии. RegAsm используется для регистрации компонента .NET в службах COM, а затем TlbExp — для экспорта библиотеки типов COM клиенту и ссылки на нее из языков не принадлежащих .NET.

Службы вызова платформы

Мы рассказали о взаимодействии между компонентами COM и .NET. Теперь поговорим о другом виде — о взаимодействии между кодом .NET и так называемом неуправляемом коде. Его обеспечивает технология, называемая службами вызова платформы, или, кратко, PInvoke.

Неуправляемый код и ненадежный код

Первое, о чем необходимо знать, — термины неуправляемый и ненадежный не являются синонимами.

Ненадежный код C# является кодом, который встроен в блок с префиксом в виде ключевого слова unsafe. Код в таком блоке может использовать весь диапазон идиом C++, таких как указатели и массивы на основе стека. Он считается ненадежным, так как такие идиомы часто ассоциируются с ошибками, но такой код по-прежнему управляется средой времени выполнения .NET.

Со своей стороны, неуправляемый код не управляется средой времени выполнения .NET. Когда поток выполнения приложения .NET входит в сегмент неуправляемого кода, среда времени выполнения .NET больше не имеет контроля над действиями этого кода, и не может обеспечить для него сборку мусора или правила безопасности (по этой причине приложения, которые используют неуправляемый код, должны наделяться доверием со стороны системного администратора).

Службы вызова платформы позволяют коду .NET взаимодействовать не только с ненадежным кодом, но и с достоверно неуправляемым.

Доступ к неуправляемому коду

Хотя .NET может взаимодействовать с неуправляемым кодом в любой DLL, чаще всего он взаимодействует с кодом в DLL, составляющим базовую функциональность Windows API. Это включает user32.dll, gdi32.dll и kernel32.dll. Процесс представления функций из этих DLL для кода .NET должен быть знаком любому, кто использовал ключевое слово Declare для предоставления вызовов Win32 API для кода VB6:

[sysimport(dll="user32.dll")]

public static extern int MessageBoxA(int Modal, string Message, string Caption, int Options);

В приведенном примере с помощью оболочки .NET вызова осуществляется вызов Windows API, который выводит окно с сообщением. В атрибуте выше функции оболочки определяется DLL, которой функция в оболочке должна делегировать свою работу. Теперь клиент кода .NET может вызывать функцию оболочки для вызова функций API:

MessageBoxA(0, "PInvoke worked!", "PInvoke Example", 0);

Хотя здесь для функции оболочки задано такое же имя, как и для вызова Windows API, в который она отображается, можно задать для нее и другое имя, как делалось в примере выше. Изменим теперь имя "MessageBox" на имя, которое более точно определяет, как будет использоваться вызов API. Сделаем это, определяя дополнительное значение в атрибуте sysimport:

[sysimport (dll="ustr32.dll", name="MessageBoxA") ]

public static extern int ErrorMessage(int Modal, string Message, string Caption, int Options);

При переименовании вызова Windows API таким образом клиенты могут вызывать функцию с новым именем:

ErrorMessage(0, "PInvoke worked!", "PInvoke.Example", 0);

Недостатки PInvoke

Мы видели, что достаточно просто ссылаться и вызывать неуправляемую функцию из кода .NET. К сожалению, существуют потенциальные недостатки использования неуправляемого кода.

Хотя Microsoft сознательно откладывает вопрос взаимодействия платформ, многие люди подозревают, что оно уже на горизонте для платформы .NET. При взаимодействии платформ можно выполнить программу .NET на любой платформе — от Macintosh до Unix при условии, что платформ? обеспечена средой времени выполнения .NET. Однако при использовании PInvoke, код .NET соединяется с операционной системой Windows.

Рассматривая использование PInvoke, прежде всего необходимо проверить, что требуемая функциональность не представлена базовым классом .NET. Если среда времени выполнения .NET будет когда-либо перенесена на другую платформу, базовые классы .NET также будут перенесены, и код получит шанс правильно выполниться на новой платформе, возможно, с небольшими изменениями.

Заключение

Как показывает эта глава, COM и .NET являются различными технологиями, которые способны работать совместно, если применяются соответствующие технологии. Используя утилиты взаимодействия, такие как TlbImp.exe, RegAsm.exe и TlbExp.exe, разработчики могут применять унаследованные компоненты COM в качестве строительных блоков для новых приложений .NET.

По сравнению с компонентами COM сборки легче создавать, развертывать и поддерживать. Маловероятно, что .NET когда-либо полностью заменит разработчикам COM, для которых скорость выполнения является основной задачей. Однако, скорее всего, разработчики приложений Web и программ, которые внутренне используются организациями, сочтут сборки .NET избавлением от ада DLL.

Главa 20

Службы COM+

Введение

В этой главе рассматриваются два больших вопроса: 1) что такое службы COM+, как они разрабатываются и как работают; и 2) каким образом службы COM+ могут использоваться на платформе .NET в целом и в C#, в частности.

Мы рассмотрим первый вопрос в первой части этой главы. Даже если вы знакомы с MTS — предшественником служб COM+, вы узнаете много нового при рассмотрении новых служб, таких как очереди сообщений и события. Как будет показано, службы COM+ предоставляют намного больше, чем поддержка транзакций, — значительный объем готовой функциональности, где каждый профессиональный программист C# может найти что-то полезное.

Последняя часть главы посвящена второму вопросу: как службы COM+ используются на платформе .NET. Мы рассмотрим классы, интерфейсы и атрибуты, которые находятся в пространстве имен EnterpriseServices, а также утилиту RegSvcs.exe.

Хотя службы COM+ могут показаться первоначально сложными, в частности, когда потребуется преодолеть барьеры взаимодействия, в конце концов выясняется, что они экономят много времени и приложения, построенные на их основе, предельно надежны.

Давайте начнем с рассмотрения того, как появились службы COM+.

Службы COM+ в ретроспективе

На заре программирования разработчик должен был создавать все с самого начала. Если ему требовались, например, возможности базы данных, то он должен был ее реализовывать, придумывая механизм для поддержания индексов и поиска записей. Тогда при создании программного обеспечения люди тратили много времени на повторное изобретение колеса.

По мере развития технологий программирования поставщики упаковали полезную функциональность в повторно используемые серверные компоненты. (Теперь, например, если разработчику потребуется функциональность базы данных, то он может использовать Oracle или SQL Server). По мере дальнейшего развития все больше полезной функциональности извлекалось из прикладных программ и переносилось на уровень серверных программ и даже операционной системы.

Можно рассматривать службы COM+ как проявление этой тенденции. Службы COM+ облегчили жизнь разработчиков уровня предприятия, предоставляя ценную функциональность, которую легко могут использовать компоненты пользователя. Когда компоненту необходима такая возможность, как обеспечение транзакции, то для получения надежного решения разработчик может положиться на службы COM+.

Состав служб COM+

Службы COM+ начинали жизнь как дополнительный модуль Windows NT, называемый Сервер транзакций Microsoft (MTS). В настоящее время Windows 2000 определяет MTS как составную часть операционной системы, переименовывая ее в ходе процесса.

В дополнение ко всем исходным свойствам MTS, службы COM+ включают в себя новые возможности, в еще большей степени сокращающие объем кода, который должен написать разработчик компонентов.

Службы COM+, которые уже были представлены в MTS, включают в себя:

□ Обеспечение транзакций

□ Организацию пулов объектов

□ Оперативную активизацию объектов (JIT, Just-in-Time)

□ Безопасность

Новые службы, введенные COM+:

□ Поддержка событий

□ Очереди сообщений компонентов

□ Выравнивание нагрузки компонентов

В этой главе мы рассмотрим каждую из служб COM+, как старые, известные из MTS, так и новые, которые могут быть еще незнакомы. Но сначала кратко рассмотрим лучшего помощника COM+: "snap-in" служб компонентов (Component Services). (Snap-in является специальным типом программы, таким как SQL Server или IIS, который выполняется внутри интерфейса консоли управления Microsoft — ММС.)

Snap-in служб компонентов

Опытные разработчики могут вспомнить, что администратор MTS был доступен в Windows NT из меню Start как пункт Option Pack. В соответствии со своим новым статусом составной части операционной системы службы COM+ перечислены в Windows 2000 более отчетливо в меню Administrative Tools под заголовком Component Services.

Левая панель окна служб компонентов (Component Services) содержит иерархическое дерево с компьютером, приложением COM+ и узлами компонентов. (На языке служб компонентов приложение является группой компонентов COM+, которые рассматриваются (администрируются) как целое, это называлось в MTS пакетом). Класс каждого компонента в приложении представлен золотым шариком со знаком плюс в середине, который начинает вращаться, когда происходит обращение к компоненту.

Существует два метода импорта сборок .NET в службы компонентов. Первый метод использует функциональность, предоставленную snap-in служб компонентов, а второй использует CLR.

Мы рассмотрим оба метода импорта позже. В данный момент начнем обзор различных служб, которые предоставляет COM+.

Транзакции COM+

Назначение транзакций

Транзакция является множеством операций, которые завершаются успешно или отказывают как целое, то есть, если одна операция в множестве отказывает, то все остальные, завершившиеся ранее, должны вернуться в исходное состояние, которое было до начала транзакции. Практический пример будет представлен ниже.

Рассмотрим web-сайт, который занимается в основном обработкой кредитных карт для заказов. Если пользователь заказывает на сайте какой-то продукт, то требуется, чтобы не только было выполнено списание с его счета, но чтобы запись заказа была помещена в базу данных заказов. Если возникает проблема с вводом записи о заказе в базу данных, то списание средств с кредитной карты должно быть отменено и заказ также должен быть отменен, иначе пользователь заплатит за товар, который он никогда не получит.

public void PlaceOrder(OrderInfo, objOrderInfo, UserInfo objUserInfo) {

CreditCard objCreditCard=new CreditCard();

OrderTable objOrderTable=new OrderTable();

// Шаг 1: Списание средств с кредитной карты

objCreditCard.PlaceCharge(objOrderInfo, objUserInfo);

// Если здесь возникает ошибка между шагами 1 и 2,

// то заказчик не получит продукт, за который

// он заплатил

// Шаг 2: Записать заказ

objOrderTablе.RecordOrder(objOrderInfo, objUserInfo);

}

В старые времена разработчики должны были создавать свои собственные схемы обеспечения транзакций, чтобы получить разновидность функциональности, упомянутой выше. Эти схемы включали обычно много логических переменных и детально разработанные стратегии обработки ошибок, и были подвержены ошибкам. Уже недавно объект ADODB.Connection предлагал поддержку транзакций в достаточно элегантной форме. Теперь, с помощью объекта ContextUtil, который мы скоро рассмотрим, службы COM+ предоставляет разработчикам механизм транзакций, который является надежным, готовым к использованию и более простым, чем подход ADODB.Connection.

Принципы транзакций

Рассмотрим внутренний механизм работы транзакций

Координатор распределенных транзакция (DTC), компонент операционной системы Windows, отвечает за управление транзакциями. Компоненты, которые могут вовлекаться и участвовать в транзакциях, должны быть согласованы с интерфейсом, понимающим DTC, и должны предоставлять менеджера ресурсов, который может отменить или зафиксировать единицы работы, когда DTC прикажет ему это сделать. Этот высокий уровень абстракции позволяет таким различным компонентам, как процессоры баз данных и процессоры кредитных карт, работать совместно.

DTC реализует транзакции с помощью процесса, называемого двухфазной фиксацией. При выполнении транзакции DTC запрашивает сначала у каждого менеджера ресурсов, участвующего в транзакции, сможет ли он завершить свою работу — это является первой фазой. И только если DTC получает положительное подтверждение от каждого участника, он приказывает каждому участнику продолжить и зафиксировать изменения — это будет вторая фаза. Если один из участников испытывает трудности в первой фазе, то DTC прикажет каждому участнику отменить свою работу.

"Контекст" является абстракцией, важной для обработки транзакций. Каждая операция транзакции — такая как списание средств с кредитной карты и последующая вставка записи о заказе — имеет контекст, с которым она ассоциирована. Если операция происходит в контексте транзакции, это равносильно тому, что операция является частью транзакции и может предложить DTC, чтобы транзакция была зафиксирована или отменена. Такая операция по сути обладает правом "вето" на выполнение всех операций в своем контексте.

Хотя и немного детальное, это объяснение внутренней работы транзакций будет полезно позже, при кодировании поддержки транзакций в сборке .NET.

Транзакции в N-звенной архитектуре

Архитектурно типичные клиент-серверные приложения, использующие транзакции COM+, состоят из слоя объектов доступа к данным, которые выполняют работу по добавлению, удалению, извлечению и обновлению записей в базе данных. Этот слой завернут в слой бизнес-объектов, которые реализуют бизнес-правила через интерфейс пользователя на основе формы Windows или на основе браузера.

Обычно один метод бизнес-объекта вызывает несколько методов на различных объектах доступа к данным. Если один из методов доступа к данным не может выполниться правильно, метод бизнес-объекта использует механизм транзакций COM+, чтобы запросить DTC об отмене операции.

Службы COM+ и время жизни объекта

Название Сервер транзакций Microsoft было несколько неправильным, так как MTS предоставлял больше, чем просто поддержку транзакций. В этом разделе мы рассмотрим две службы COM+, которые были упомянуты первыми в MTS, — активацию JIT и создание пулов объектов. Обе эти службы являются технологиями эффективного использования серверными машинами своих ресурсов при манипуляциях серверными объектами.

Чтобы понять, как работают активация JIT и создание пулов объектов, необходимо знать, что существуют два различных типа приложений COM+.

□ Библиотечное приложение является совокупностью классов компонентов, объекты которых создаются в процессе вызывающего клиента.

□ Серверное приложение является совокупностью классов компонентов, объекты которых создаются в выделенном замещающем процессе, отдельном от процессов всех вызывающих клиентов.

Большинство приложений на основе ASP используют компоненты, хранимые в серверных приложениях. Так как компоненты находятся в выделенном замещающем процессе, отказ серверного компонента не приводит к аварийному отказу сервера Web. Для стандартных (не определенных в .NET) приложений COM задается свойство Activation Type на вкладке Activation окна приложения Properties:

Как мы увидим позже, существует другая процедура для определения типа активации сборки .NET. Будет показано, как делать это программным путем с помощью атрибутов.

Только приложения COM+ с типом активации Serverapplication могут воспользоваться пулом объектов, включающих службу COM+, которая будет рассмотрена далее.

Создание пулов объектов

В терминах циклов процессора и байтов является достаточно дорогим создание экземпляра и инициализация объекта. Эти расходы объединяются для сервера Web, который должен обслуживать одновременно десятки тысяч пользователей. Чтобы пользователи не ощущали задержки, пока сервер Web пытается создать объекты компонентов, службы COM+ предоставляют пулы объектов.

Как можно предположить, эта служба COM+ позволяет поддерживать пул объектов, которые создаются и ожидают использования даже до того, как они понадобятся. Когда серверному сеансу понадобиться объект определенного компонента он просто запрашивает объект из пула доступных объектов, получая его немедленно, если объект готов, или ожидая в очереди, пока не станет доступен новый объект. Когда объект освобождается клиентом, службы COM+ не разрушают его, а позволяют существовать в пуле, готовым на случай, если он понадобится другому клиенту. Так как можно определить минимальное и максимальное число объектов в пуле, то можно дискретно контролировать, сколько серверных ресурсов выделено для класса компонента.

По своей природе пул объектов может позволить использовать один и тот же экземпляр объекта нескольким различным клиентским процессам. Это означает, что если один из этих процессов изменит состояние общего объекта перед тем, как откажется от него, новое состояние будет видно следующему процессу, который получит объект, даже если это состояние будет не тем, которое ожидает наследующий процесс. Существует пара способов обойти эту проблему.

Первый: можно спроектировать классы компонентов не имеющих состояния. Это означает, что для них не определяются свойства, которые должны быть заданы перед вызовом методов. Вместо этого значения этих свойств хранятся вне объектов, в самом интерфейсе пользователя, и передаются в качестве входящих в объекты аргументов, когда вызываются их методы. Так как объекты больше не связаны с какими-либо устойчивыми данными, эта схема эффективно сокращает классы до библиотек функций.

Второй: можно наделить компоненты интеллектом, необходимым для сериализации и восстановления ими своего состояния.

Состояние объекта представляет интерес не только при использовании пулов объектов, но и при использовании утилит активации JIT. Это следующая рассматриваемая служба COM+.

Оперативная активизация (JIT)

Поскольку создание экземпляров объектов тратит серверные ресурсы, разработчики вынуждены учитывать это. В частности, они должны гарантировать, что интенсивно используемые многопользовательские программы с большим объемом трафика создают объекты только, когда требуется, и удаляют их из памяти сразу после использования. К счастью для нас, службы COM+ предоставляют другой подход, который освобождает разработчика от этих проблем.

При оперативной активизации разработчик может создать экземпляры своих объектов один раз в начале программы и затем использовать их, когда они понадобятся, не беспокоясь о ресурсах, которые они потребляют в спящем режиме. Неявно службы COM+ освобождают пространство, занимаемое объектами, когда они не используются, и восстанавливают их в тот момент, когда клиентский код вызывает их методы. Таким образом, клиентский код может содержать ссылки на множество объектов сколько угодно времени, уверенный, что службы COM+ предоставят объекты, когда понадобится, и освободят их память, как только будет возможно.

Подобно классам в пулах, активированные JIT классы должны разумно управлять состоянием. Если службы COM+ освобождают пространство, занимаемое объектом между последовательными обращениями к нему, то нет гарантии, что те же значения свойств будут сохраняться между первым и вторым вызовом.

Мы использовали серверные объекты, которые требуют некоторой инициализации перед тем, как можно будет вызвать их методы. Объект ADODB.Connection является одним из примеров необходимо инициализировать его свойство ConnectionString, прежде чем можно будет приказать ему выполнить запрос SOL. Так как объект в пуле не может сохранять состояние между вызовами, невозможно инициализировать его и вызывать его методы в отдельных шагах. Обращение к объекту в пуле должно передавать в этот объект все значения, которые нужны ему для выполнения работы.

Безопасность

Существует два аспекта в модели безопасности, которую предоставляют службы COM+

Первым аспектом является аутентификация. Вкратце службы COM+ позволяют наложить ограничения на того, кто имеет доступ к служебным компонентам и методам, которые они предоставляют. Используя snap-in службы компонентов, можно задать уровень аутентификации приложения для определения того, когда выполняется аутентификация — при соединении клиента с серверным объектом, для каждого сетевого пакета коммуникации для объекта, при вызове каждого метода и т.д.

Вторым аспектом модели безопасности служб COM+ является уровень заимствования прав компонента. Так как серверный объект выполняет работу от имени клиента, то может быть полезно предоставлять для серверного объекта привилегии доступа и идентичности клиента, которого он обслуживает. Уровень заимствования прав позволяет это сделать.

Безопасность на основе ролей является соглашением, связываемым обычно с клиент-серверными приложениями, которые используют службы COM+. При таком подходе серверный объект проверяет сначала, принадлежит ли его клиент определенной роли безопасности Windows, прежде чем выполнять работу от его имени.

Мы обсудим безопасность на основе ролей для сборки .NET позже в этой главе.

Новые службы COM+

До сих пор мы говорили о службах COM+, которые могут быть уже знакомы из MTS. Теперь давайте перейдем к обсуждению служб, введенных вместе с COM+.

События

Одной из новых служб, которую предлагает COM+, является механизм событий, архитектура которого отличается от традиционного механизма использования точек соединения.

Эту новую службу событий часто называют "моделью издателя-подписчика". При таком подходе разрабатывается интерфейс события и затем он регистрируется в службах COM+. Затем регистрируются классы, которые хотят иметь возможность инициировать события, определенные в интерфейсе как издатели. После этого регистрируются классы, которые хотят иметь возможность обрабатывать события, определенные в интерфейсе события как подписчики. Когда объект издателя/сервера инициирует событие, службы COM+ отвечают за уведомление всех подписчиков. Так как классы-подписчики не соединены напрямую с классами-издателями, а вместо этого используют службы COM+ в качестве посредника, то архитектуру часто описывают как "слабосвязанные" события.

Чтобы реализовать эту схему, необходимо выполнить следующие шаги:

1. Создать DLL класса события, которая реализует интерфейс события.

2. Зарегистрировать DLL класса события в службах COM+.

3. Создать серверный компонент, который внутренне создает экземпляр класса события и вызывает методы на этом экземпляре, чтобы инициировать события.

4. Зарегистрировать серверный компонент как издателя в службах COM+.

5. Создать клиентские компоненты, которые реализуют интерфейс события, чтобы перехватывать события.

6. Зарегистрировать клиентские компоненты как подписчиков в службах COM+.

Когда эти шаги завершены, класс издателя может инициировать событие, просто создавая экземпляр класса события и вызывая один из его методов. Как отмечено выше, службы COM+ сообщат каждому классу-подписчику, что было инициировано событие. Однако, несмотря на надежность такого подхода, существует, по крайней мере, два недостатка в способе, которым службы COM+ реализуют эту модель событий.

□ Первый: так как объекты подписчики уведомляются об инициированных событиях по одному за раз, объект каждого подписчика имеет потенциальную возможность ждать других, если его обработчик событий работает медленно.

□ Второй: по крайней мере во время написания книги службы COM+ не имели возможности инициировать события от объектов-издателей для объектов-подписчиков на других машинах.

Повторим, что основное достоинство архитектуры событий типа издатель-подписчик состоит в том, что классы издателя и подписчика остаются слабосвязанными, способными общаться, не поддерживая прямых ссылок друг на друга.

Очереди сообщений

В ходе выполнения программы возникают специальные условия. Может отказать сервер базы данных или пользователь попытается, намеренно или случайно, отправить задание с отсоединенного терминала. Обычно разработчики предусматривают специальные меры предосторожности в прикладном коде, чтобы учитывать такие аномалии.

Служба очередей сообщений из COM+ позволяет разработчикам отказаться от кодирования ситуаций отсутствия соединения. Вкратце, служба очередей записывает вызовы методов от клиентских объектов к серверным объектам, которые недоступны, так что они могут быть отправлены назад серверному объекту, когда он снова будет доступен в сети. Клиентский код остается в полном неведении, что произошло что-то неординарное и что службы COM+ действовали в качестве посредника.

Как можно представить, очереди сообщений будут удобным средством при создании приложений, которые должны выполняться на машинах со связью и без связи. К тому же, очереди сообщений являются составной частью сервера BizTalk компании Microsoft — новой серверной программы, делающей возможным процесс перемещения данных внутри и между организациями. При установке Windows 2000 Server очереди сообщений являются одной из возможностей, которую можно установить или отбросить.

Несмотря на свои достоинства, очереди сообщений имеют также серьезные ограничения. Очевидно, что когда службы COM+ ставят сообщение в очередь к недоступному серверному объекту и возвращают управление клиенту, они не могут вернуть сложный ответ. По этой причине необходимо принимать в расчет возможность возникновения неподтвержденных ошибок при проектировании компонентов, которые используют очереди сообщений. Более того, нельзя использовать значения, возвращенные из компонентов очереди, для выполнения обработки. Если компоненты отключены, они не могут возвращать значения.

Выравнивание нагрузки компонентов

Даже с теми возможностями, которые предоставляют пулы объектов, а также оперативная активизация для максимального доступа серверных ресурсов, возникают ситуации, когда одна серверная машина просто не может обслужить всех прикладных клиентов. В таких случаях разработчики должны воспользоваться службой COM+ выравнивания нагрузки компонентов. Эта служба распределяет прикладные объекты по ферме совместно действующих серверов Web, чтобы ни один сервер не был перегружен объектными запросами и чтобы конечные пользователи продолжали работать с высокой производительностью.

Основным объектом стратегии выравнивания нагрузки компонентов является сервер выравнивания нагрузки компонентов, или CLB (Component Load Balancing). CLB является машиной Windows Advance Server или Windows Data Server, которая служит менеджером для других серверов в ферме. CLB отвечает за распределение запросов объектов между доступными серверами.

Алгоритм, который использует сервер CLB для выбора хоста объекта, является достаточно сложным. Он проходит в определённом порядке список доступных серверов, передавая запросы создания первому доступному серверу. Так как этот список упорядочен от наиболее надежного к наименее надежному серверу, скорее всего запрос будут обрабатывать более мощные серверы.

Когда установлено соединение между клиентским приложением и серверной машиной, которую CLB присвоил клиенту, коммуникация продолжается между ними без последующего вмешательства CLB. Так как не существует гарантии, что объект сервера клиента будет находиться на все той же серверной машине, компоненты не должны иметь состояния, и таким образом, пулы объектов недоступны.

Использование служб COM со сборками .NET

Теперь когда мы разобрались с различными службами COM+, давайте посмотрим, как службы могут использоваться со сборками .NET. Мы представим обзор общей технологии, и рассмотрим детали работы конкретных служб в последующих разделах. В конце главы мы научимся использовать транзакции, безопасность на основе ролей, пулы объектов и активацию JIT из компонентов .NET.

Взаимодействие со службами COM+ из сборок .NET делается возможным в основном через атрибуты. Задавая префиксы для определений классов с помощью атрибутов, определенных в пространстве имен EnterpriseServices, можно определить, как службы COM+ используют эти классы. Компилятор C# знает, как транслировать атрибуты в "крючки" необходимого кода, которые службы COM+ ожидают от компонентов.

Некоторыми из атрибутов, определенных в пространстве имен EnterpriseServices, являются:

□ Transaction

□ ObjectPooling

□ JustInTimeActivation

□ EventClass

□ ApplicationActivation

В дополнение к этим атрибутам пространство имен EnterpriseServices определяет различные классы и перечисления, некоторые из которых мы скоро подробно рассмотрим. Если надо увидеть содержимое пространства имен, воспользуйтесь утилитой WinCV. Чтобы увидеть классы в пространстве имен System.EnterpriseServices, добавьте строку:

<assembly name = "System.EnterpriseServices" />

в элемент <wincv> файла WinCV.exe.config.

Подготовка сборок .NET для служб COM+

Вероятно, можно согласиться, что атрибуты являются достаточно хорошим ненавязчивым подходом для использования служб COM+ в классах .NET. Нужно просто вставлять их в начале подходящих классов, не так ли? К сожалению, все не так просто, как кажется. Давайте начнем с предварительных шагов, которые необходимо предпринять, чтобы подготовить классы для служб COM+.

Предоставление атрибутов сборок

Первое. Компания Microsoft предлагает стандартизованное множество "атрибутов сборок", которые должны включаться в каждую сборку .NET, использующую службы COM+.

Следующий пример кода перечисляет их:

[assembly:ApplicationActivation(ActivationOption.Server)]

[assembly:ApplicationID("448934a3-324f-34d3-2343-129ab3c43b2c")]

[assembly:ApplicationName("SomeApplicationName")]

[assembly:Description("Description of your assembly here.")]

Рассмотрим каждый из этих атрибутов по очереди.

Ранее упоминалось, что существуют два вида приложений COM+ — серверные приложения и библиотечные приложения. Первый атрибут в коде примера — ApplicationActivation — позволяет определить, каким из этих видов приложений является определенная сборка. (Допустимые значения для этого атрибута определяются в перечислении ActivationOption, которое можно заметить внутри скобок атрибута.) Определяя тип приложения программным путем с помощью этого атрибута, можно избежать необходимости открывать менеджер службы компонентов и делать это вручную. Это перечисление имеет два значения: ActivationOption.Library и ActivationOption.Server.

Второй атрибут, ApplicationID, определяет присоединенный 128-битный уникальный идентификатор (GUID) сборки. (GUID являются идентификационными номерами, которые гарантируют уникальность в течение очень большого периода времени. Службы COM+ ожидают такой идентификатор от каждого приложения.) В коде примера случайно выбранный GUID не имеет ничего существенного, он присутствует только для целей демонстрации. Для каждой создаваемой сборки придется создавать свой собственный. Чтобы сделать это, можно использовать утилиту GuidGen.exe компании Microsoft, которая распространяется вместе с Visual Studio.

Третий атрибут в коде примера, ApplicationName, позволяет определить имя приложения службы COM+, которое будет создано для размещения сборки .NET, когда она импортируется в службы COM+. В данном примере используется значение SomeAppliсationName.

Четвертый и последний атрибут, ApplicationDescription, позволяет связать описание со сборкой, чтобы предоставить разработчикам некоторые идеи о том, что она делает.

Документация компании Microsoft определяет, что любая сборка .NET, использующая в соединении со службами COM+, должна применять все эти четыре атрибута.

Развертывание сборки для служб COM+

Развертывание сборки, использующейся со службами COM+, будет ненамного труднее, чем развертывание любой другой сборки .NET.

Первое: необходимо предоставить сборке сильное имя. Это делается с помощью утилиты sn.exe из SDK .NET (см. главу 10). sn.exe будет выводить файл сильного имени, на который можно ссылаться из командной строки, когда сборка компилируется, чтобы встроить сильное имя в компилированную сборку.

Второе: необходимо зарегистрировать сборку в глобальном кэше сборок (см. главу 10).

Если сборку будут использовать только управляемые клиенты (то есть, клиенты .NET), никаких дополнительных усилий по развертыванию не требуется. Когда управляемый клиент создает в сборке экземпляр обслуживаемого класса, CLR использует атрибуты в сборке для автоматической регистрации компонента в службах COM+.

Однако, если классы в сборке используются неуправляемым кодом, необходимо самостоятельно явно зарегистрировать сборку в службах COM+ до выполнения любой клиентской программы. Программа для выполнения этой регистрации, RegSvcs.exe, предоставляется компанией Microsoft как часть SDK .NET. Когда RegSvcs выполняется на компоненте .NET, она создает приложение COM+ с именем, указанным атрибутом ApplicationName в сборке, и импортирует сборку в него.

Для чего же требуется RegSvcs.exe?

Как можно помнить из предыдущей главы по взаимодействию COM, сборки .NET имеют архитектуру, отличную от архитектуры компонентов COM. Задача RegSvcs.exe состоит в разрешении этих различий, чтобы сборки .NET удовлетворяли интерфейсу, ожидаемому службами COM+. Чтобы выполнить свою работу, утилита RegSvcs.exe проделывает четыре вещи.

1. Загружает и регистрирует сборку .NET.

2. Создает библиотеку типов для сборки .NET.

3. Импортирует библиотеку типов в приложение служб COM+.

4. Использует метаданные внутри DLL, чтобы правильно сконфигурировать библиотеку типов внутри приложения служб COM+.

RegSvcs не только заботится обо всех деталях импортирования сборки в службы COM+, но предоставляет также достаточно хороший контроль за тем, как это происходит. Этот контроль обеспечивается в форме дополнительных параметров командной строки. Вот синтаксис команды:

Regsvcs .NetComponentName [COM+AppName] [TypeLibrary.tlb]

С помощью второго аргумента (COM+AppName) можно определить другое имя для создаваемого приложения COM+, предоставляя второй аргумент командной строки при вызове RegSvcs. Для еще большей гибкости можно определить имя файла библиотеки типов, которая создается при предоставлении третьего аргумента (TypeLibrary.tlb). Желательно всегда предоставлять эти аргументы при вызове RegSvcs, так как более ранние версии этой программы будут молчаливо перезаписывать любые существующие файлы, которые могут иметь такие же имена, как у вновь создаваемых файлов.

Предварительные итоги

Теперь мы знаем, как подготовить сборку .NET для применения вместе со службами COM+. Эта подготовка включает в себя:

□ Снаряжение сборки рекомендованными атрибутами сборки

□ Соединение классов прокси с внутренними "рабочими" классами посредством атрибута ComEmulate

□ Развертывание сборок с помощью sn.exe, al.exe и, возможно, RegSvcs.exe

Имея общую информацию, перейдем к обсуждению использования конкретных служб COM+ из сборок .NET. Начнем с транзакций.

Использование транзакций со сборками .NET

Существуют две вещи, которые необходимо сделать, чтобы подготовить класс .NET для транзакций. Первое: необходимо изменить прокси класса с помощью атрибута для указания его уровня поддержки транзакций. Второе: необходимо добавить в класс код для управления его поведением, когда он участвует в транзакциях.

Вспомним концепцию "контекста" транзакции, которая была рассмотрена выше. Она играет здесь важную роль, поэтому к ней можно вернуться для быстрого повторения.

Определение транзакционной поддержки

Ранее при использовании транзакций из служб COM+ можно было увидеть настройку уровня транзакций в окне свойств класса в Snap-In службы компонентов. Эта настройка позволяет задать уровень поддержки транзакций, который службы COM+ будут предоставлять стандартному компоненту COM.

Иначе в .NET уровень поддержки транзакций в сборке можно определить не с помощью графического окна в snap-in службы компонентов, а программным путем с помощью атрибута Transaction, определенного в пространстве имен EnterpriseServices. В примере ниже мы определяем, что следующий класс прокси должен поддерживать транзакции. При заданном значении атрибута компонент будет сконфигурирован для поддержки транзакций, когда он импортируется в службы COM+ с помощью RegSvcs.exe.

[Transaction(TransactionOption.Supported)]

public class ProxyClass:ServicedComponent {

}

Supported является только одним из нескольких значений, которые можно присвоить атрибуту Transaction компонента. Фактически, существует четыре значения, которые представлены в перечислении TransactionOption, являющемся частью пространства имен System.EnterpriseServices.

□ Когда атрибут Transaction класса задан как Disabled, службы COM+ не предоставляют транзакционной поддержки для класса, даже если такая поддержка определена где-то в коде. (Другими словами, вызовы этого класса, сделанные для ContextUtil с целью фиксации или отмены транзакций, игнорируются. Мы познакомимся с ContextUtil в следующем разделе.)

□ Когда атрибут Transaction класса задан как NotSupported, такой класс не вовлекается в транзакции, запускаемые его клиентами, другими словами он не помещается в их контекст. В данной конфигурации объекты этого класса не определяют, будет ли вызываемая транзакция фиксироваться или отменяться.

□ Когда атрибут Transaction класса задан как Supported, объекты этого класса могут вовлекаться в контекст транзакций своих вызывающих клиентов, если эти вызывающие клиенты на самом деле начинают транзакцию. Такой объект не может самостоятельно порождать транзакцию.

□ Когда атрибут Transaction класса задан как Required, службы COM+ знают, что объекты этого класса могут выполняться только в контексте транзакции. Если такой объект вызывается клиентом, имеющем транзакционный контекст, объект наследует контекст транзакции клиента. Если, однако, объект вызывается клиентом, который не имеет транзакционного контекста, службы COM+ создают контекст для этого объекта.

□ Когда атрибут Transaction класса задан как RequiresNew, службы COM+ создают новую транзакцию для класса каждый раз, когда он вызывается. Даже если клиент объекта уже имеет транзакцию, службы COM+ создают новую транзакцию для серверного объекта. Как можно догадаться, классы, сконфигурированные подобным образом, способны отменить только свои собственные транзакции, а не работу своих клиентов.

На практике большинство разработчиков используют только одну или две из этих настроек. Значение Supported подходит для классов типа класса Settings , которому нужно будет обслуживать классы с транзакциями и без транзакций. Для большинства других транзакционных классов можно справиться, задавая значение Required. Однако все-таки может возникнуть ситуация, где потребуются одно или несколько составных значений, дополнительную информацию можно найти в книге "Professinal Windows DNA Programming" (ISBN 1-861004-45-1) издательства Wrox Press.

Кодирование транзакций с помощью ContextUtil

Модификация класса с помощью атрибута Transaction является только частью того, что необходимо сделать, чтобы подготовить его для участия в транзакции. Надо также определить, как каждый метод в этом классе ведет себя, когда он вызывается как часть транзакции. Это осуществляется с помощью класса ContextUtil из пространства имен System.EnterpriseServices.

Проще говоря, класс ContextUtil предоставляет контекст транзакции. Когда имеется ссылка на контекст транзакции, можно явно заставить этот контекст зафиксировать или отменить транзакцию. Методы, которые необходимо вызвать для фиксации или отмены транзакций, представляются как методы класса в классе ContextUtil, поэтому не придется создавать экземпляр класса ContextUtil, чтобы их вызвать.

В качестве примера сделаем краткий обзор фрагмента кода, приведенного ниже.

public bool PlaceOrder(bool CommitTrans) {

// Попытка работы

try {

if (CommitTrans) {

// Эта транзакция должна быть зафиксирована

// шаг 1 — увеличить число единиц продукта ID=2 на 10

IncreaseUnits(2, 10);

// шаг 2 — сократить запас продукта ID=2 на 10 единиц

ReduceStock(2, 10);

} else {

// Эта транзакция должна быть отменена

// шаг 1 — увеличить число единиц продукта ID=5 на 5 единиц

IncreaseUnits(5, 5);

// шаг 2 - сократить запас продукта ID=5 на 5 единиц

ReduceStock(5, 5);

}

// Если все прошло хорошо, завершить транзакцию.

ContextUtil.SetComplete();

return true;

}

// Этот код выполняется, если встречается ошибка.

catch (Exception е) {

// Отменить работу, которую выполнила эта функция.

ContextUtil.SetAbort();

return false;

}

}

Что здесь происходит?

Мы имеем две транзакции, которые обрабатываются в зависимости от значения CommitTrans. Для любой транзакции PlaceOrder() вызывает два метода, которые оба соединяющиеся с базой данных Northwind, чтобы сделать изменения в таблице Product. Метод ReduceStock() сокращает объем запасов в столбце UnitsInStock, метод IncreaseUnits() увеличивает значение столбца UnitsOnOrder(). Для обоих методов первым параметром является ProductID в строке, которую нужно изменить, второй параметр есть величина, на которую мы хотим изменить соответствующий столбец.

Выполняющаяся транзакция контролируется булевой переменной CommitTrans, передаваемой в PlaceOrder(). Первая транзакция должна быть зафиксирована, так как уровень запаса для ProductID=2 равен 17, следовательно, можно удалить десять элементов и все еще иметь оставшийся запас. Однако вторая транзакция обречена на отказ так как ProductID=5 не имеет запаса элементов и существует ограничение на столбец UnitsInStock, которое не позволяет значению становиться меньше нуля. Это означает, что можно проверить, будет ли транзакция отменяться или нет. Не должно быть никаких проблем с вызовом IncreaseStock(), поэтому можно увидеть, что транзакция была отменена, проверяя значение столбца UnitsOnOrder для ProductID=5.

В блоке try, если все идет хорошо, или, другими словами, если поток выполнения должен покинуть PlaceOrder() нормально, через return true, инструкция PlaceOrder() вызывает метод SetComplete() объекта ContextUtil, эффективно сообщая DTC через менеджер ресурсов, что в той части, которая касается его, транзакцию необходимо зафиксировать.

С другой стороны, если где-то в PlaceOrder возникает ошибка и порождается исключение, управление программой будет передано предложению catch(). В этом предложении PlaceOrder() вызовет метод SetAbort() объекта ContextUtil. Этот метод посылает голос PlaceOrder() за отмену транзакции, в которую он вовлечен, и DTC после получения этого голоса от менеджера ресурсов прикажет каждому участнику транзакции отменить свою работу.

Помните, что не требуется создавать экземпляр объекта ContextUtil, чтобы вызвать его методы SetComplete() и SetAbort(). Эти методы класса, поэтому их можно вызывать прямо на классе.

Практически любой код, поддерживающий транзакции будет походить на код в примере. Он вызывает SetComplete() перед своей точкой выхода для фиксации всей работы, которая была успешно выполнена, или он вызывает метод SetAbort() класса ContextUtil в своем обработчике ошибок, чтобы все отменить в связи с ошибкой. Существует, правда, еще более простой способ.

Компания Microsoft предоставляет атрибут .NET, называемый AutoComplete. Методы, модифицированные с помощью этого атрибута, автоматически применяют подход, описанный выше. И хотя такие методы никогда явно не ссылаются на класс ContextUtil, они неявно завершают свои транзакции, если те заканчиваются нормально, или отменяют всю работу если выход происходит в связи с ошибкой (когда порождается исключение). По прежнему необходимо вызывать SetAbort(), чтобы отменить работу транзакции если порождается исключение.

[AutoComplete]

public bool PlaceOrder(bool CommitTrans) {

try {

if (CommitTrans) {

// Эта транзакция должна быть зафиксирована

// шаг 1 — Увеличить число единиц продукта ID=2 на 10

IncreaseUnits(2, 10);

// шаг 2 - Сократить запас продукта ID=2 на 10 единиц

ReduceStock(2, 10);

} else {

// Эта транзакция должна быть отменена

// шаг 1 - Увеличить число единиц продукта ID=5 на 5

IncreaseUnits(5, 5);

// шаг 2 — Сократить запас продукта ID=5 на 5 единиц

ReduceStock(5, 5);

}

return true;

} catch (Exception e) {

ContextUtil.SetAbort();

return false;

}

}

Это полный код примера всей транзакции, он показывает, как все части объединяются. Следующее просто встроено в библиотеку классов с заданным сильным именем и зарегистрировано в глобальном кэше сборок.

using System;

using System.EnterpriseServices;

using System.Data.SqlClient;

namespace OrderTransaction {

[Transaction(TransactionOptiоn.Required)]

public class Purchase : ServicedComponent {

public Purchase() { }

public bool PlaceOrder(bool CommitTrans) {

// Попытка работы

try {

if (CommitTrans) {

// Эта транзакция должна быть зафиксирована

// шаг 1 - Увеличить число единиц продукта ID=2 на 10

IncreaseUnits(2, 10);

// шаг 2 - Сократить запас продукта ID=2 на 10 единиц

ReduceStock(2, 10);

} else {

// Эта транзакция должна быть отменена

// шаг 3 — Увеличить число единиц продукта ID=5 на 5

IncreaseUnits(5, 5);

// шаг 2 — Сократить запас продукта ID=5 на 5

единиц ReduceStock(5, 5);

}

// Если все прошло хорошо, закончить транзакцию.

ContextUtil.SetComplete();

return true;

}

// Этот код выполняется, если встречается ошибка.

catch (Exception e) {

// Отменить работу, которую выполнила эта функция.

ContextUtil.SetAbort();

return false;

}

}

public void ReduceStock(int ProductID, int amount) {

string source = "server ephemeral;uid=sa;pwd=garysql;database Northwind";

SqlConnection conn = new SqlConnection(source);

string command =

"UPDATE Products SET UnitsInStock = UnitsInStock - " +

amount.ToString() + " WHERE ProductID = " + ProductID.ToString();

conn.Open;

sqlCommand cmd = new SqlCommand(command, conn);

cmd.ExecuteNonQuery();

conn.Close();

}

public void IncreaseUnits(int ProductID, int amount) {

string source = "server=ephemeral;uid=sa;pwd=garysql;database=Northwind";

SqlConnection conn = new SqlConnection(source);

string command =

"UPDATE Products SET UnitsOnOrder = UnitsOnOrder +

" amount.ToString() + " WHERE ProductID = " + ProductID.ToString();

conn.Open();

SqlCommand cmd = new SqlCommand(command, conn);

cmd.ExecuteNonQuery();

conn.Close();

}

public void Restore() {

// Восстановить запас продукта ID=2

ReduceStock(2, -10);

// Восстановить единицы продукта для ID=2

IncreaseUnits(2, -10);

// Не требуется восстанавливать запас или единицы продукта для ID=5,

// так так транзакция должна быть отменена

}

}

}

Можно создать клиента для тестирования этой библиотеки классов. Здесь клиент создан так, что он ожидает, пока пользователь проверит содержимое базы данных, прежде чем восстановить базу данных в исходное состояние. Это позволяет увидеть результаты транзакций.

statiс void Main(string[] args) {

Purchase order = new Purchase();

Console.WriteLine("\nThis transaction should commit");

Console.WriteLine("ProductID = 2, ordering 10 items");

if (Order.PlaceOrder(true)) Console.WriteLine("Transaction Successful");

else Console.WriteLine("Transaction Unsuccessful");

Console.WriteLine("\nThis transaction should roll back");

Console.WriteLine("ProductID = 5, ordering 5 items");

if (Order.PlaceOrder(false)) Console.WriteLine("Transaction Successful");

else Console.WriteLine("Transaction Unsuccessful");

Console.WriteLine(

"\nTake a look at the database then hit enter to

+ "return database to original state");

Console.ReadLine();

Order.Restore();

}

Другие полезные методы ContextUtil

Рассмотрим еще пару методов класса ContextUtil которые могут оказаться полезны при программировании на C#.

Первый метод IsCallerInRole() предназначен для безопасности на основе ролей. В качестве входной переменной этот метод получает строковую переменную, содержащую имя определенной роли системы безопасности Windows 2000. Он возвращает булево значение, указывающее, является или нет пользователь, который в данный момент вызывает объект, членом указанной роли.

В примере кода ниже добавлена проверка, чтобы убедиться, что пользователь, пытающийся вызвать PlaceOrder(), является авторизованным членом роли Administrators. Если пользователь не является членом этой роли, то PlaceOrder() порождает исключение.

[AutoComplete]

public bool PlaceOrder(bool CommitTrans) {

if (!ContextUtil.IsCallerInRole("Administrators") {

throw new AccessViolationException("User is not authorized to place" + "orders.");

}

// Поместить код транзакции здесь

}

Вторым полезным методом класса ContextUtil является IsInTransaction(). Этот метод возвращает булево значение, указывающее, участвует ли объект в данный момент в транзакции.

Профессиональным программистам C# приходится иногда разрабатывать транзакционные компоненты для удаленной установки, которую они не контролируют. Чтобы убедиться, что сборки, требующие транзакционной поддержки, правильно для нее сконфигурированы, можно вызвать свойство IsInTransaction класса ContextUtil и инициировать ошибку, если это свойство задано как false.

В примере кода ниже свойство IsInTransaction используется для гарантии, что сборка правильно сконфигурирована, прежде чем ей будет разрешено ей начать какую-либо работу. Код порождает исключение, если IsInTransaction имеет значение false. Можно протестировать это, изменяя атрибут класса на TransactionalOptionDisabled.

[AutoComplete]

public bool PlaceOrder(bool CommitTrans) {

if (!ContextUtil.IsInTransaction) {

throw new

ConfigurationException("This assembly needs to be configured for" + " transactions.");

}

// Выполнить транзакцию

}

Этим мы завершаем обсуждение транзакций COM+ и класса ContextUtil. Давайте перейдем к пулам объектов.

Использование пудов объектов со сборками .NET

Нетрудно сконфигурировать компонент .NET для пула объектов. Для этого необходимо изменить класс с помощью атрибута и реализовать интерфейс в этом классе.

Атрибут ObjectPooling

Атрибутом с помощью которого необходимо изменить класс, является ObjectPooling. Этот атрибут получает четыре аргумента.

1. Аргумент Enabled является первым. Ему должно быть присвоено значение true.

2. Аргумент MinPoolSize определяет минимальное число экземпляров объектов, которое должны поддерживать службы COM+ в пуле объектов класса.

3. Аргумент MaxPoolSize определяет максимальное число экземпляров объектов, которое должны поддерживать службы COM+ в пуле объектов класса.

4. Аргумент CreationTimeOut определяет период времени, в течение которого службы COM+ должны пытаться получить объект из пула, прежде чем вернуть отказ.

Далее следует пример атрибута ObjectPooling со всеми четырьмя аргументами, примененными к классу. Мы расширим этот фрагмент кода в конце данного раздела.

[ObjectPooling (Enabled=True, MinPoolSize=1, MaxPoolSize=100, CreationTimeout=30)]

public class CreditCard:ServicedComponent {

Интерфейс ServicedComponent

Как можно было заметить, класс в примере выше наследует интерфейс ServicedComponent. Все классы .NET, которые используют пулы объектов, должны реализовывать этот интерфейс. ServicedComponent содержит три метода для переопределения.

1. Метод CanBePooled() используется клиентами для определения, что может быть создан пул объектов класса.

2. Метод Activate() вызывается службами COM+ на объекте в пуле перед тем, как этот объект передается новому клиенту.

3. Метод Deactivate(), напарник метода Activate(), вызывается службами COM+, когда объект освобождается клиентом, чтобы вернуть его в пул доступных объектов.

Следующий фрагмент кода показывает класс, сконфигурированный для пула объектов.

[ObjectPooling (Enabled=true, MinPoolSize=1, MaxPoolSize=100, CreationTimeout=30)]

public class CreditCard:ServicedComponent {

// Этот метод будет вызываться службами COM+ для определения,

// что объект находится в пуле.

public override bool CanBePooled() {

return true; // необходимо вернуть логическое "true"

}

// Этот метод должен вызываться службами COM+, когда объект

// передается клиенту.

public override void Activate() {

// Код инициализации находится здесь.

}

// Этот метод будет вызываться службами COM+, когда

// объект будет возвращаться в пул.

public override void Deactivate() {

// Код завершения находится здесь

}

// Этот метод будет вызываться клиентом.

public void PlaceCharge(int OrderInfo, int UserInfo) {

// код списания средств с кредитной карты находится здесь

}

}

Как показывает пример, атрибут ObjectPooling и интерфейс ServicedComponent требуются для того, чтобы класс .NET реализовал пул объектов. Также можно заметить, что в отличие от атрибута Transaction атрибут ObjectPooling применяется непосредственно к "рабочей" сборке .NET, а не к классу прокси, созданному с атрибутом ComEmulate, который был рассмотрен ранее в этой главе.

Использование активизации JIT со сборками .NET

Чтобы сконфигурировать класс .NET для активизации JIT, нужно просто изменить класс с помощью атрибута JustInTimeActivation, задав булево значение true. В данном случае класс CreditCard из предыдущего примера модифицируется для активизации JIT.

[JustInTimeActivation(true)]

public class CreditCard: ServicedComponent {

// Этот метод будет вызываться клиентом.

public void PlaceCharge(OrderInfo objOrderInfo, UserInfo objUserInfo) {

// Код для снятия средств с кредитной карты находится здесь

}

}

Заключение

Прежде чем начинать разработку следующего проекта развития предприятия, познакомьтесь со службами COM+. Упомянутая ранее книга "The Professional Windows DNA" издательства WroxPress является хорошим началом. При правильном использовании службы COM+ дают изобилие функциональности, что потребовало бы немало времени для воспроизведения и еще больше для полной отладки. Более того, подходы, которые службы COM+ используют для поддержки транзакций, сохранения ресурсов и межпроцессной коммуникации являются в некоторой степени базовыми, после изучения их можно будет применять к большому спектру разнообразных проблем.

Глава 21

Графические возможности GDI+

Это вторая из двух глав в книге, описывающая элементы взаимодействия непосредственно с пользователем, т.е. вывод информации на экран и прием пользовательского ввода с помощью мыши или клавиатуры. В главе 9 мы рассматривали формы Windows и узнали, как выводить диалоговое окно или окна SDI и MDI и как разместить в них различные элементы управления, такие как кнопки, текстовые поля и поля списков. Основное внимание было уделено использованию элементов управления на основе их способности взять на себя полную ответственность за собственное изображение на устройстве вывода. Необходимо только задать свойства элемента управления и добавить методы обработки событий. Стандартные элементы управления — очень мощные, можно получить сложный интерфейс пользователя, используя только их. На самом деле они сами по себе вполне достаточны для полного интерфейса при работе со многими приложениями, в частности диалогового типа или с интерфейсом пользователя типа проводника.

Однако существуют ситуации, в которых простые элементы управления не дают гибкости, требуемой в интерфейсе пользователя. Иногда необходимо ввести текст заданным шрифтом в точной позиции окна или нарисовать изображения, не используя элемент управления "графическая кнопка", простые контуры или другую графику. Хорошим примером является программа Word for Windows. В верхней части экрана находятся различные кнопки и панели инструментов, которые используются для доступа к различным свойствам Word. Некоторые из меню и кнопок вызывают диалоговые окна или даже списки свойств. Эта часть интерфейса пользователя была рассмотрена в главе 9. Но основная часть экрана в Word for Windows будет совершенно другой. Окно SDI выводит представление документа. Оно имеет текст, аккуратно расположенный в нужных местах и выведенный с использованием множества размеров и шрифтов. В документе может быть выведена любая диаграмма и, если посмотреть на документ компоновки для печати, поля реальных страниц также должны быть выведены. Ничего из этого нельзя сделать с помощью элементов управление, описанных в главе 9. Чтобы изобразить такой вывод, программа Word for Windows должна взять на себя прямую ответственность за сообщение операционной системе, что необходимо вывести и где в его окне SDI. Как это делается, объясняется в данной главе. Здесь будет показано, как рисовать различные элементы, включая:

□ Линии, простые контуры

□ Изображения из растровых и других файлов изображении

□ Текст

Во всех случаях элементы могут быть нарисованы где угодно в области экрана, занимаемой приложением, и код непосредственно управляет рисованием, например, когда и как обновить элементы, каким шрифтом изобразить текст и так далее.

В этом процессе необходимо использовать множество вспомогательных объектов, включая перья (используемые для определения характеристик линий), кисти (для определения того, как заполняются области, каким цветом — сплошные, в виде решетки или заполнены в соответствии с некоторым другим шаблоном) и шрифты (определяющие форму символов текста). Также будет рассказано, как устройства интерпретируют и выводят различные цвета.

Код, требуемый для реального рисования на экране, часто достаточно прост и базируются на технологии, называемой GDI+. GDI+ состоит из множества базовых классов .NET, доступных для выполнения произвольного рисования на экране. Эти классы могут отправить необходимые инструкции драйверам графических устройств для обеспечения правильного вывода в требуемых местах экрана монитора (или для печати твердой копии). Также как и остальные базовые классы .NET, классы GDI+ основываются на вполне доступной для понимания и использования объектной модели.

Объектная модель GDI+ концептуально достаточно проста, но все же требуется хорошее понимание описанных ниже принципов того, как Windows организует изображение элементов на экране, чтобы эффективно и рационально использовать GDI+.

Эта глава делится на два основных раздела. Первые две трети главы посвящены концепциям, лежащим в основе GDI+, исследуется, как происходит рисование с упором на теорию. Здесь будет представлено довольно много примеров, почти все из которых являются небольшими приложениями, выводящими специфические жестко закодированные элементы (в основном простые фигуры, такие как прямоугольники и эллипсы). В последней трети главы мы сконцентрируемся на разработке развернутого примера, называемого CapsEditor, который выводит содержимое текстового файла и позволяет пользователям делать некоторые изменения в выводимых данных. Назначение этого примера состоит в демонстрации принципов рисования, применяемых на практике в реальном приложении. Для реального рисования обычно необходим небольшой код — классы GDI+ работают на достаточно высоком уровне, поэтому в большинстве случаев требуется только несколько строк кода для рисования одиночного элемента (например, изображения или фрагмента текста). Но хорошо спроектированное приложение, использующее GDI+, будет выполнять за сценой большую дополнительную работу, т.е. обеспечивать эффективность рисования, и, если потребуется, обновление экрана без каких-либо лишних изображений. (Это важно, так как основная часть работы по рисованию требует от приложения высокой производительности.) Пример CapsEditor показывает, как делать большую часть этого фонового управления.

Библиотека базовых классов GDI+ очень велика, и мы едва сможем прикоснуться к их свойствам в данной главе. Это обдуманное решение, так как попытка охватить хотя бы минимальную часть доступных классов, методов и свойств превратит эту главу в справочное руководство, которое просто перечисляет классы и т. д. Более важно понять фундаментальные принципы, вовлеченные в рисование, а затем можно будет исследовать доступные классы самостоятельно. (Полные списки всех доступных в GDI+ классов и методов имеются в документации MSDN). Разработчики, имеющие дело с VB, найдут, скорее всего, концепции, вовлеченные в рисование, совершенно незнакомыми, так как фокус VB заключается в элементах управления, обрабатывающих свое собственное рисование. Разработчики с подготовкой C++/MFC окажутся в более выгодном положении, так как MFC требует в большей степени внешнего управления процессом рисования, используя предпроцессор GDI+, GDI. Однако даже при хорошем знакомстве с GDI подавляющая часть материала окажется новой. GDI+ в действительности является оболочкой GDI, но тем не менее GDI+ имеет объектную модель, которая скрывает большую часть работы GDI. В частности GDI+ заменяет почти полностью модель с состоянием GDI, в которой элементы выбирались в контексте устройства, на модель, менее учитывающую состояние, в которой каждая операция рисования происходит независимо. Объект Graphics (представляющий контекст устройства) является единственным объектом, существующим между операциями рисования.

Кстати, в этой главе термины рисование и черчение взаимозаменяемы и описывают процесс вывода некоторого элемента на экране или на другом устройстве вывода.

Прежде коротко перечислим основные пространства имен, которые встречаются в базовых классах GDI+.

Пространство имен

Содержимое

System.Drawing

Большинство классов, структур, перечислений, а также делегатов, связанных с базовой функциональностью рисования.

System.Drawing.Drawing2D

Более специализированные классы, предоставляющие развитые эффекты при рисовании на экране.

System.Drawing.Imaging

Различные классы, которые задействованы при манипуляции изображениями (битовые файлы, файлы GIF и т.д.).

System.Drawing.Printing

Вспомогательные классы, специально предназначенные для случая, когда в качестве устройства "вывода" указан принтер или окно предпросмотра печати.

System.Drawing.Design

Некоторые предопределенные диалоговые окна, списки свойств и другие элементы интерфейса пользователя, связанные с расширением интерфейса пользователя во время проектирования.

System.Drawing.Text

Классы для выполнения более развитых манипуляций со шрифтами и семействами шрифтов.

Почти все классы, структуры и т.д., использующиеся в этой главе, взяты из пространства имен System.Drawing.

Основные принципы рисования

В этом разделе исследуются основные принципы, которые необходимо знать, чтобы начать рисовать на экране. Мы начнем с обзора GDI — описанной ниже технологии, на которой основывается GDI+, и посмотрим, как она связана с GDI+. Затем будет рассмотрено несколько простых примеров.

GDI и GDI+

Одним из достоинств Windows и современных операционных систем в целом является возможность абстрагировать детали работы определенных устройств от разработчика. Например, нет необходимости знать что-либо о драйвере устройства жесткого диска, чтобы программным путем прочитать или записать файлы на диск, достаточно просто вызвать соответствующие методы в подходящих классах .NET (или до появления .NET в эквивалентных функциях Windows API). Этот принцип также вполне справедлив, когда речь идет о рисовании. Когда компьютер рисует что-нибудь на экране, он делает это, посылая инструкции видеоплате с указанием, что рисовать и где. Проблема в том, что на рынке существует много сотен различных видеокарт, сделанных различными производителями. Если принять это в расчет и писать в приложении специальный код для каждого видеодрайвера, который рисует что-то на экране, создание приложения станет практически невозможной задачей. Именно поэтому интерфейс графического устройства (GDI) операционной системы Windows всегда присутствовал в системе с самых первых версий Windows.

GDI скрывает нюансы работы различных видеоплат, так что для выполнения определенной задачи вызывается просто функция API Windows, и внутренне GDI вычисляет, как заставить определенную видеоплату сделать то, что требуется. Однако большинство компьютеров имеют более одного устройства, на которое можно послать вывод. Сегодня это монитор, доступ к которому получают через видеоплату, и принтер. Некоторые машины могут иметь более одной видеоплаты или более одного принтера. GDI проявляет удивительное искусство, заставляя принтер выглядеть так же, как экран, с позиции приложения. Если необходимо напечатать что-то, а не вывести это на экран, то система просто информируется, что устройством вывода является принтер, а затем вызываются те же функции API. Таким образом, истинное предназначение GDI — абстрагировать свойства оборудования на относительно высоком уровне API.

GDI предоставляет для разработчиков относительно высокий уровень API, но это по-прежнему API, который основывается на старом API Windows с функциями в стиле С, и поэтому его не так просто использовать. GDI+ в большой степени позиционируется как слой между GDI и приложением, предоставляя более интуитивно-понятную объектную модель на основе наследования. Хотя GDI+ является по сути оболочкой вокруг GDI, компания Microsoft смогла с помощью GDI+ предоставить новые свойства и при этом повысить производительность.

Контексты устройств и объект Graphics

В GDI устройство, на которое должен направиться вывод, идентифицируется с помощью объекта, известного как контекст устройства (DC — Device Context). Контекст устройства хранит информацию об определенном устройстве и может транслировать вызовы функций API GDI в инструкции, которые необходимо послать на это устройство. Можно также запрашивать контекст устройства, чтобы определить возможности соответствующего устройства (например, может ли принтер печатать в цвете или осуществляет только черно-белую печать), чтобы настроить соответственно вывод. Если запросить устройство сделать что-то, на что оно не способно, контекст устройства обычно это обнаруживает и совершает соответствующее действие (которое, в зависимости от ситуации, может означать порождение ошибки или изменение запроса, чтобы получить ближайшее соответствие тому, на что действительно способно устройство).

Однако контекст устройства имеет дело не только с аппаратным устройством. Оно действует как мост к Windows и способен поэтому учесть любые требования или ограничения, налагаемые на рисование операционной системой Windows. Например, если Windows знает, что необходимо перерисовать только часть окна приложения (возможно, потому что было минимизировано другое окно, которое скрывает часть приложения), контекст устройства может перехватывать и аннулировать попытки рисовать вне этой области. Благодаря связи контекста устройства с Windows работа через контекст устройства может упростить код и другими способами. Например, аппаратным устройствам необходимо сообщать, где рисовать объекты, и обычно координаты задаются относительно верхнего левого угла экрана (или устройства вывода). Но приложение будет считать рисование чем-то происходящим в определенной позиции внутри клиентской области своего собственного окна. (Клиентская область в Windows является частью окна, которая обычно используется для рисования, что означает окно с исключенными границами, таким образом во многих приложениях клиентская область будет областью с белым фоном.) Поскольку окно может быть расположено где угодно на экране и пользователь вправе перемещать его в любое время, трансляция между двумя координатами является потенциально трудной задачей. Но контекст устройства всегда знает, где находится окно, и способен выполнить эту трансляцию автоматически. Это означает, что можно запросить контекст устройства нарисовать элемент в определенной позиции окна, не беспокоясь о том, где на экране в настоящее время расположено окно приложения.

Как можно видеть, контекст устройства является очень мощным объектом, и нет ничего удивительного в том, что в GDI все рисование выполняется через него. Контекст устройства используется даже иногда для операций, которые не включают рисование на экране или на любом аппаратном устройстве. Например, если имеется битовое изображение, в котором делаются некоторые изменения (возможно, изменение его размера), то более эффективно делать это с помощью контекста устройства, так как он может использовать некоторые аппаратные свойства машины, чтобы выполнять такие операции быстрее. Изменение изображений находится вне рамок этой главы, но заметим, что контексты устройств очень эффективно подготавливают изображения в памяти, прежде чем конечный результат посылается на экран.

При использовании GDI+ контекст устройства по-прежнему существует, хотя ему теперь дано другое имя. Он завернут в базовый класс .NET с именем Graphics. При чтении этой главы можно будет заметить, что большая часть рисования делается с помощью вызовов методов на экземпляре Graphics. Фактически, так как класс System.Drawing.Graphics отвечает за реальную обработку большинства операций рисования, очень немногое делается в GDI+, что не включает экземпляр Graphics. Умение управлять этим объектом, является ключом к пониманию того, как рисовать на устройствах вывода с помощью GDI+.

Пример: рисование контуров

Мы собираемся начать с короткого примера для рисования в основном окне приложения. Все примеры в этой главе созданы с помощью Visual Studio.NET как приложения Windows на C#. Вспомните, что для проекта такого типа мастер кода определяет класс с именем Form1, производный от System.Windows.Form, который представляет основное окно приложения. Если не утверждается обратное, то во всех примерах новый или измененный код означает код, добавленный к этому классу.

При использовании .NET, когда речь идет о приложениях, которые выводят различные элементы управления, термин форма как правило заменяет термин окна для представления прямоугольного объекта, занимающего область экрана от имени приложения. В этой главе будет использоваться термин окно, так как в контексте рисуемых вручную элементов он более уместен.

В первом примере создается просто форма и на ней рисуется в методе InitializeComponent(). Необходимо сказать вначале, что это не лучший способ рисования на экране, мы быстро обнаружим, что возникает проблем в связи с невозможностью перерисовать что-нибудь после запуска. Однако пример проиллюстрирует достаточно много деталей рисования, не требуя при этом большой работы.

В данном случае мы запускаем Visual Studio.NET, создаем приложение Windows и изменяем код в методе InitializeComponent() следующим образом:

private void InitializeComponent() {

this.components = new System.ComponentModel.Container();

this.Size = new System.Drawing.Size(300, 300);

this.Text = "Display At Startup";

this.BackColor = Color.White;

и добавляем следующий код в конструктор Form1:

public Form1() {

InitializeComponent();

Graphics dc = this.CreateGraphics();

this.Show();

Pen BluePen = new Pen(Color.Blue, 3);

dc.DrawRectangle(BluePen, 0, 0, 50, 50);

Pen RedPen = new Pen(Color.Red, 2);

dc.DrawEllipse(RedPen, 0, 50, 80, 60);

}

Это единственные изменения, которые необходимо сделать. Наш пример является примером DisplayAtStartup из загружаемого кода.

Фоновый цвет формы задается как белый, поэтому она выглядит "подходящим" окном, в котором мы собираемся вывести графическое изображение. Соответствующая строка кода помещается в метод InitializeComponent(), чтобы Visual Studio .NET распознал строку и мог изменить внешний вид формы. Иначе можно было бы использовать графическое представление для задания цвета фона, что привело бы к появлению той же самой инструкции в InitializeComponent(). Вспомните, что этот метод используется системой Visual Studio.NET для создания представления формы. Если не задать явно цвет фона, то он останется с заданным по умолчанию для диалоговых окон цветом, объявленным в настройках Windows.

Затем мы создаем объект Graphics с помощью метода CreateGraphics(). Этот объект Graphics содержит контекст устройства Windows, который нужен для рисования. Созданный контекст устройства ассоциируется с устройством вывода, а также с этим окном. Отметим, что здесь используется переменная с именем dc для экземпляра объекта Graphics, отражая тот факт, что он в действительности представляет контекст устройства, действующий за сценой.

Далее для выведения окна вызывается метод Show(). Это делается просто для немедленного вывода окна, так как на самом деле нельзя выполнить рисования, пока окно не будет изображено — не на чем будет рисовать.

Наконец, мы выводим прямоугольник в точке с координатами (0, 0) с шириной и высотой равной 50 и эллипс в точке с координатами (0, 50) с шириной 80 и высотой 50. Отметим, что координаты (х, у) означают х пикселей вправо и у пикселей вниз от верхнего левого угла клиентской области окна и являются координатами верхнего левого угла изображаемой фигуры:

Запись (х, у) является стандартной математической записью, очень удобной для описания координат. Используемые методы DrawRectanglе() и DrawEllipse() имеют по 5 параметров. Первый параметр каждого метода — это экземпляр класса System.Drawing.Pen. Pen является одним из ряда поддерживаемых объектов, помогающих при рисовании,— объект содержит информацию о том, как должны быть нарисованы линии. Наше первое перо определяет, что линии должны быть голубыми с шириной 3 пикселя, второе говорит что линии красные и имеют ширину 2 пикселя. Последние четыре параметра являются координатами. Для прямоугольника они представляют координаты (х, у) верхнего левого угла, а также его ширину и высоту, задаваемые числом пикселей. Для эллипса эти числа представляют те же самые вещи, за исключением того, что речь идет о гипотетическом прямоугольнике, в который вписывается эллипс, а не о самом эллипсе.

Более подробно эти новые структуры и методы объекта Graphics будут рассмотрены позже.

Выполнение данного кода приведет к следующему результату:

Экран иллюстрирует два момента. Первое: можно ясно видеть, что понимается под клиентской областью окна. Это белая область, которая была задействована в результате задания свойства BackColor. Отметим, что прямоугольник расположился в углу этой области, как и можно было ожидать при задании координат (0, 0). Второе, отметим, что верхняя часть эллипса слегка перекрывает прямоугольник,— это было трудно предвидеть из заданных в коде координат. Так, Windows размещаем линии, ограничивающие прямоугольник и эллипс. По умолчанию Windows будет пытаться поместить линию на границе фигуры что не всегда можно сделать точно, так как линия должна проходить через пиксели (очевидно), но граница каждой фигуры теоретически лежит между двумя пикселями. В результате линии толщиной в один пиксель будут проходить точно внутри верхней и левой сторон фигуры, но вне нижней и правой сторон, значит, фигуры, которые, строго говоря, находятся рядом друг с другом, будут иметь границы, перекрывающиеся на один пиксель. Мы определили более широкие линии, поэтому перекрытие будет больше. Можно изменить поведение по умолчанию, задавая свойство Pen.Alignment, как отмечено в документации MSDN, но для наших целей достаточно поведения по умолчанию.

На рисунке показано также, что код сработал правильно. Кажется, что рисование невозможно выполнить проще. К сожалению, если действительно выполнить этот пример, можно будет заметить, что форма ведет себя немного странно. Все нормально, если просто оставить ее на месте или перемещать по экрану с помощью мыши. Попробуйте минимизировать форму а затем восстановить ее. Окажется, что нарисованные формы просто исчезнут. То же самое произойдет, если протащить другое окно через окно этого примера. Еще интереснее будет, если закрыть форму другим окном, так что оно закроет только часть фигур, а затем убрать его снова. Окажется, что временно скрытая часть исчезла и осталась только часть эллипса и часть прямоугольника.

Что же делать? Проблема возникает, если окно или его часть становятся скрытыми по какой-либо причине (например, оно минимизируется или закрывается другим окном), Windows обычно немедленно отбрасывает всю информацию, относящуюся к тому, что там было изображено. Система должна это делать, иначе используемая память для хранения данных экрана будет слишком большой. Типичный компьютер может работать с видеоплатой, настроенной для вывода 1024×768 пикселей, возможно, в режиме с 24-битовым цветом. Мы покажем, что означает 24-битовый цвет позже, но в данный момент можно сказать, что каждый пиксель на экране занимает 3 байта, т. е. 2.25 Мбайт для изображения экрана. Однако нет ничего необычного, если пользователь имеет во время работы более 10 минимизированных окон в своей панели задач, в худшем сценарии 20, каждое из которых занимает весь экран, если оно не минимизировано. Если бы Windows действительно хранил визуальную информацию, которую содержат эти окна, готовую на случай, если пользователь захочет их восстановить, то речь шла бы о 45 Мбайт. Сегодня хорошая графическая карта может иметь 64 Мбайта памяти, но еще пару лет назад 4 Мбайта считалось большим объемом для графической платы, а избыточные данные необходимо было хранить в основной памяти компьютера. Множество людей все еще используют старые машины. Очевидно, что для Windows было бы непрактично управлять интерфейсом своих пользователей подобным образом.

В тот момент, когда какая-либо часть окна становится скрытой, эти пиксели пропадают. Windows замечает, что окно (или некоторая его часть) скрыто, и когда система обнаруживает, что эта область больше не является скрытой, она запрашивает приложение, которое владеет окном, чтобы оно перерисовало его содержимое. Существуют исключения из этого правила, обычно для случаев, когда небольшая часть окна скрыта очень недолго (хорошим примером является выбор элемента из основного меню, когда этот элемент меню раскрывается внизу, временно закрывая часть окна ниже). Однако можно ожидать, что, если часть окна становится скрытой, то приложению придется перерисовать его позже.

Это проблема для нашего примера приложения. Мы поместили код рисования в конструктор Form1, который вызывается только один раз при запуске приложения, и невозможно вызывать конструктор снова, чтобы перерисовать фигуры, когда это потребуется позже.

В главе 9 при рассмотрении элементов управления нам ничего этого не нужно было знать, так как стандартные элементы управления достаточно развиты и могут правильно перерисовать себя, когда Windows об этом попросит. Это одна из причин, почему при программировании элементов управления вообще не нужно беспокоиться о реальном процессе рисования. Если мы берем на себя ответственность в приложении за рисование на экране, то нам нужно также гарантировать, что приложение будет отвечать правильно, когда Windows попросит перерисовать все или часть окна. В следующем разделе мы изменим пример так, чтобы это было возможно.

Рисование фигур с помощью OnPaint

Если приведенное выше объяснение заставило вас подумать, что рисование своего собственного пользовательского интерфейса будет очень сложной задачей, то не стоит волноваться. Это не так. Были приведены различные детали процесса, для того чтобы вы поняли, с какими проблемами столкнетесь. Но заставить приложение перерисовать себя, когда необходимо, — в действительности легкая задача.

Когда возникает необходимость, Windows уведомляет приложение, что требуется выполнить некоторую перерисовку изображения, инициируя событие Paint. Интересно то, что класс Form уже реализовал обработчик для этого события, поэтому не нужно создавать свой собственный. Можно воспользоваться этой архитектурой, исходя из факта, что обработчик Form1 для события Paint будет вызывать в процессе обработки виртуальный метод OnPaint(), передавая в него единственный параметр PaintEventArgs. Это означает, что для выполнения рисования необходимо просто переопределить метод OnPaint(). Мы создадим для этого новый пример, называемый DrawShapes. Как и раньше, определяем DrawShapes как приложение Windows, генерируемое с помощью Visual Studio.NET, и добавим следующий код класса Form1:

protected override void OnPaint(PaintEventArgs e) {

Graphics dc = e.Graphics;

Pen BluePen = new Pen(Color.Blue, 3);

dc.DrawRectangle(BluePen, 0, 0, 50, 50);

Pen RedPen = new Pen(Color.Red, 2);

dc.DrawEllipse(RedPen, 0, 50, 80, 60);

base.OnPaint(e);

}

Отметим, что метод OnPaint() объявлен как protected. OnPaint() обычно используется внутри класса, поэтому нет необходимости любому другому коду вне класса знать о его существовании.

PaintEventArgs является производным классом от EventArgs, используемого обычно для передачи информации о событиях. PaintEventArgs имеет два дополнительных свойства, из которых наиболее важным является экземпляр Graphics, уже настроенный и оптимизированный для рисования требуемой части окна. Это означает, что нам не нужно вызывать CreateGraphics(), чтобы получить контекст устройства в методе OnPaint(), — он уже существует. Мы вскоре рассмотрим другое дополнительное свойство, оно содержит более подробную информацию о том, какая область окна действительно нуждается в перерисовке.

В данной реализации метода OnPaint() мы сначала получаем ссылку на объект Graphics из PaintEventArgs, затем рисуем фигуры так же, как это делалось раньше. В конце вызывается метод OnPaint() базового класса. Этот шаг является важным. Мы переопределили OnPaint() для выполнения нашего собственного рисования, но возможно, что Windows должен выполнить свою собственную работу в процессе рисования, и такая работа будет связана с методом OnPaint() в одном из базовых классов .NET.

Для этого примера может оказаться, что удаление вызова base.OnPaint() не оказывает никакого влияния на работу, но никогда не пытайтесь удалить вызов. Это может привести к некорректному завершению работы Windows и непредсказуемым результатам.

OnPaint() будет также вызываться, когда приложение впервые запускается и окно приложения выводится в первый раз, поэтому нет необходимости дублировать код рисования в конструкторе, хотя по-прежнему нужно задать здесь цвет фона и все другие свойства формы. Это обычно задается либо добавлением команды явно, либо заданием цвета в окне свойств Visual Studio.NET:

private void InitializeComponent() {

this.components = new System.ComponentModel.Container();

this.Size = new System.Drawing.Size(300, 300);

this.Text = "Draw Shapes";

this.BackColor = Color.White;

}

Выполнение этого кода вначале дает те же результаты, что и в предыдущем примере, за исключением того, что теперь приложение ведет себя правильно, когда окно минимизируется или скрывается его часть.

Использование области вырезания

Пример приложения DrawShapes из предыдущего раздела иллюстрирует основные принципы, используемые при рисовании в окне, однако оно не очень эффективно. Причина в том, что оно пытается рисовать в окне все, независимо от того, сколько должно быть перерисовано. Рассмотрим ситуацию, показанную на следующем рисунке. После выполнения приложения DrawShapes было открыто другое окно, которое закрыло часть формы DrawShapes.

До сих пор все хорошо. А что произойдет, когда перекрывающее окно (в данном случае Task Manager) будет удалено, так что окно DrawShapes будет снова полностью видно? Windows, как обычно, пошлет форме событие Paint, чтобы она себя перерисовала. Прямоугольник и эллипс находится в верхнем левом углу клиентской области и поэтому видны все время, так что в действительности нет ничего, что требуется сделать в данном случае, помимо перерисовки белой фоновой области. Однако Windows этого не знает. В той степени, в какой это касается Windows, необходимо перерисовать часть окна. Это означает, что надо инициировать событие Paint, которое вызывается в нашей реализации OnPaint(). OnPaint() будет затем пытаться излишне перерисовать прямоугольник и эллипс.

В таком случае фигуры не нужно перерисовывать. Причина этого связана с контекстом устройства. Ранее говорилось, что контекст устройства внутри объекта Graphics, передаваемого в метод OnPaint(), будет оптимизирован операционной системой Windows для выполнения конкретной ближайшей задачи. Это означает, что Windows предварительно инициализирует контекст устройства информацией о том, какая область в действительности должна быть перерисована. Это прямоугольник, который был покрыт окном Task Manager на снимке экрана, приведенном выше. Во время использования GDI, область помеченная для перерисовки, называлась недействительной областью, но с появлением GDI+ терминология существенно изменилась, и эта область стала называться областью вырезания. Контекст устройства знает, какая это область, и поэтому прервет любые попытки рисования вне этой области и не будет передавать соответствующие команды рисования графической плате. Это звучит хорошо, но все равно здесь существует потенциальная потеря производительности. Мы не знаем какой объем обработки потребуется выполнить контексту устройства, чтобы определить, что рисование произойдет вне недействительной области. В некоторых случаях это может оказаться существенной величиной, так как определение того, какие пиксели необходимо изменить и в какой цвет может оказаться очень трудоемким (хотя хорошая графическая плата обеспечит аппаратное ускорение). Прямоугольник является достаточно легкой фигурой. Эллипс — труднее, поскольку необходимо вычислять положение кривой. Вывод текста потребует еще больших усилий, так как необходимо обрабатывать информацию в шрифте, чтобы определить форму каждой буквы, а каждая буква состоит из ряда линий и кривых, которые должны быть нарисованы по отдельности. Если, как в большинстве распространенных шрифтов, это будет шрифт с переменной шириной, т. е., когда у каждой буквы нет фиксированного размера и она занимает столько места, сколько ей требуется, то невозможно даже определить, сколько пространства займет текст, не выполнив предварительных громоздких вычислений

Вследствие этого запрос экземпляра Graphics выполнит некоторое рисование вне недействительной области, что почти наверняка будет бесполезным использованием процессорного времени и замедлит работу приложения. В хорошо спроектированном приложении код активно помогает контексту устройства, выполняя несколько простых проверок, чтобы убедиться, что обычная работа по рисованию действительно нужна, прежде чем вызывать подходящие методы экземпляра Graphics. В этом разделе мы создадим новый пример DrawShapesWithClipping, изменяя для этого пример DisplayShapes. В коде OnPaint() будет сделана простая проверка достоверности, что недействительная область пересекла область рисования, и только в этом случае вызовутся методы рисования.

Сначала необходимо получить данные области вырезания. Для этого используется дополнительное свойство PaintEventArgs. Свойство, называемое ClipRectangle, содержит координаты предназначенной для перерисовывания области, помещенные в экземпляр структуры System.Drawing.Rectangle. Rectangle является достаточно простой структурой, она содержит 4 представляющих интерес свойства: Top, Bottom, Left и Right. Они соответственно содержат вертикальные координаты верха и низа прямоугольника и горизонтальные координаты левого и правого краев.

Затем надо решить, какой тест будет использоваться для рисования. Здесь будет использован простой тест. Отметим, что прямоугольник и эллипс полностью содержатся внутри прямоугольника, который простирается в клиентской области от точки (0, 0) до точки (80, 130), в действительности до точки (82, 132), так как мы знаем, что линии могут отклоняться примерно на пиксель вне этой области. Поэтому будем проверять, что верхний левый угол области вырезания находится внутри этого прямоугольника. Если это так, то выполняется рисование. Если нет, то ничего не делается. Код выглядит следующим образом:

protected override void OnPaint(PaintEventArgs e) {

Graphics dc = e.Graphics;

if (e.ClipRectangle.Tор < 132 && e.ClipRectangle.Left < 82) {

Pen BluePen = new Pen(Color. Blue, 3);

dc.DrawRectangle(BluePen, 0, 0, 50, 50);

Pen RedPen = new Pen(Color.Red, 2);

dc.DrawEllipse(RedPen, 0, 50, 80, 60);

}

base.OnPaint(e);

}

Заметим, что изображение получится точно таким же, как и раньше, но производительность повысится благодаря раннему выявлению некоторых случаев, когда ничего не должно рисоваться. Отметим также, что мы выбрали достаточно примитивный тест необходимости рисования, более точный тест мог бы проверять по отдельности, нужно ли рисовать прямоугольник или эллипс, или оба объекта. Здесь существует некоторое балансирование. Можно сделать проверку в OnPaint() более сложной, в этом случае повысится производительность, но код OnPaint() при этом усложнится и потребует больше работы для своего создания. Однако почти всегда стоит провести некоторую проверку просто потому, что это помогает понять общую картину (например, в нашем примере мы узнали дополнительно, что рисунок никогда не выходит за пределы прямоугольника (0, 0) на (82, 132)). Экземпляр Graphics не имеет этого знания, он слепо следует командам рисования. Такое дополнительное знание означает, что имеются более полезные или эффективные проверки, чем те. что мог бы делать экземпляр объекта Graphics.

Измерение координат и областей

В последнем примере мы встретили базовую структуру Rectangle, которая используется для представления координат прямоугольника. GDI+ в действительности использует несколько аналогичных структур для представления координат или областей. Мы рассмотрим основные структуры, определенные в пространстве имен System.Drawing:

Структура

Основные открытые свойства

struct Point

X, Y

struct PointF

X, Y

struct Size

Width, Height

struct SizeF

Width, Height

struct Rectangle

Left, Right, Top, Bottom, Width, Height, X, Y, Location, Size

struct RectangleF

Left, Right, Top, Bottom, Width, Height, X, Y, Location, Size

Отметим, что многие эти объекты имеют ряд других свойств, методов или перезагруженных операторов, не перечисленных здесь. Рассмотрим только самые важные.

Point и PointF

Рассмотрим сначала Point (точка) Эта структура концептуально является простейшей и математически полностью эквивалентна двумерному вектору. Она содержит два открытых целых свойства, которые представляют горизонтальное и вертикальное смещение от определенного места (возможно, на экране). Посмотрите на рисунок:

Чтобы перейти из точки А в точку В, необходимо сместиться на 20 единиц вправо и на 10 единиц вниз, помеченных как X и Y на рисунке, так как это обычное обозначение. Можно было бы создать структуру Point, которая представляет это, следующим образом:

Point АВ = new Point(20, 10);

Console.WriteLine("Moved {0} across, {1} down", AB.X, AB.Y);

X и Y являются свойствами чтения-записи, а значит, можно также задать значения в Point следующим образом:

Point АВ = new Point();

AB.X = 20;

АВ.Y = 10;

Console.WriteLine("Moved (0) across, (1) down", AB.X, AB.Y);

Отметим, что хотя обычно горизонтальные и вертикальные координаты обозначаются как координаты х и у (буквы нижнего регистра), соответствующие свойства Point обозначаются X и Y (буквами верхнего регистра), так как обычное соглашение в C# для открытых свойств требует, чтобы их имена начинались с букв верхнего регистра.

PointF по сути идентична Point, за исключением того, что X и Y имеют тип float вместо int. PointF используется, когда координаты не обязательно являются целыми значениями. Для этих структур определено преобразование типов, поэтому можно неявно преобразовывать из Point в PointF и явно из PointF в Point (последнее преобразование явное в связи с риском ошибок округления):

PointF ABFloat = new PointF(20.5F, 10.9F);

Point AB = (Point)ABFloat;

PointF ABFloat2 = AB;

Одно последнее замечание о координатах. В нашем обсуждении Point и PointF сознательно присутствует неопределенность в отношении единиц измерения. Можно говорить о 20 пикселях вправо и 10 пикселях вниз или о 20 дюймах, или 20 милях. Интерпретация координат полностью принадлежит разработчику.

По умолчанию GDI+ будет представлять единицы измерения как пиксели на экране (или принтере, в зависимости от графического устройства), именно таким образом методы объекта Graphics будут представлять любые координаты, которые передаются им в качестве параметров. Например, точка Point(20, 10) представляет 20 пикселей вправо по экрану и 10 пикселей вниз. Обычно эти пиксели измеряются от верхнего левого угла клиентской области окна, как было до сих пор в рассмотренных примерах. Но это не всегда так, в некоторых ситуациях может потребоваться нарисовать относительно верхнего левого угла всего окна (включая границу) или даже относительно верхнего левого угла экрана. В большинстве случаев, однако, если документация не говорит обратное, можно предполагать, что речь идет о пикселях относительно верхнего левого угла клиентской области.

Мы вернемся к рассмотрению этого вопроса после изучения прокрутки экрана, когда речь пойдет об использовании трех различных координатных систем: мировых, страницы и устройства.

Size и SizeF

Подобно Point и PointF размеры выступают в двух вариантах. Структура Size предназначена для работы с целыми значениями, SizeF — для значений с плавающей точкой. В остальном Size и SizeF идентичны. Мы сосредоточимся здесь на структуре Size.

Во многом Size аналогична структуре Point. Она имеет два целых свойства, которые представляют горизонтальное и вертикальное расстояния, основное различие состоит в том, что вместо X и Y эти свойства называются Width и Height. Можно представить предыдущую диаграмму с помощью кода:

Size АВ = new Size(20, 10);

Console.WriteLine("Moved {0} across, {1} down", AB.Width, AB.Height);

Строго говоря структура Size математически представляет то же, что и Point, но концептуально она предназначена для использования немного другим образом. Point применяется, если говорится о местоположении объекта, a Size — когда речь идет о размере чего-то.

В качестве примера рассмотрим нарисованный ранее прямоугольник с координатой вверху слева (0, 0) и размером (50, 50):

Graphics dc. = е.Graphics;

Pen BluePen = new Pen(Color.Blue, 3);

dc.Rectangle(BluePen, 0, 0, 50, 50);

Размер этого прямоугольника равен (50, 50) и может быть представлен экземпляром Size. Нижний правый угол также находится в точке (50, 50), но будет представляться экземпляром Point. Чтобы увидеть различия, предположим, что мы рисуем прямоугольник в другом месте, так что его верхняя левая координата будет (10, 10).

dc.DrawRectangle(BluePen, 10, 10, 50, 50);

Теперь нижний правый угол имеет координаты (60, 60), но размер не изменился — по-прежнему (50, 50).

Дополнительный оператор был перезагружен для точек и размеров так, чтобы можно было добавлять размер к точке задавал другую точку:

static void Main(string [] args) {

Point TopLeft = new Point (10, 10);

Size RectangleSize = new Size(50, 50);

Point BottomRight = TopLeft + RectangleSize;

Console.WriteLine("TopLeft = " + TopLeft);

Console.WriteLine("BottomRight = " + BottomRight);

Console.WriteLine("Size = " + RectangleSize);

}

Этот код, выполняемый как простое консольное приложение, создает следующий вывод:

Отметим, что этот вывод показывает также, как метод ToString() объектов Point и Size был переопределен для вывода значения в формате {X, Y}.

Аналогично можно вычесть Size из Point, чтобы задать Point, или складывать два размера Size, задавая другой размер Size. Однако невозможно сложить точку Point с другой точкой Point. Компания Microsoft определила, что такое действие не имеет концептуального смысла, поэтому было решено не создавать никакою перезагружаемого оператора + который бы позволял это сделать.

Можно также явно преобразовать Point в Size и наоборот:

Point TopLeft = new Point(10, 10);

Size S1 = (Size)TopLeft;

Point P1 = (Point)S1;

При этом преобразовании значению S1.Width присваивается значение TopLeft.X, а S1.Height — TopLeft.Y. Следовательно, S1 содержит (10, 10). P1 будет содержать те же значения, что и TopLeft.

Rectangle и RectangleF

Эти структуры предcтавляют прямоугольную область (обычно на экране). Так же, как и в случае с Point и Size, мы рассмотрим только структуру Rectangle. RectangleF по сути идентична, за исключением того, что свойства, представляющие размеры, используют float, в то время как в Rectangle использует int.

Rectangle можно рассматривать как точку в верхнем левом углу прямоугольника и Size, которая представляет его размер. Один из его конструкторов действительно получает Point и Size в качестве параметров, Можно увидеть это переписывая предыдущий код рисования прямоугольника

Graphics dc = е Graphics;

Pen BluePen = new Pen(Color Blue, 3);

Point TopLeft = new Point(0, 0);

Size HowBig = new Size(50, 50);

Rectangle RectangleArea = new Rectangle(TopLeft, HowBig);

dc.DrawRectangle(BluePen, RectangleArea);

Этот код также использует альтернативное переопределение Graphics.DrawRectangle(), который получает Pen и структуру Rectangle в качестве своих параметров.

Можно также создать Rectangle, используя значения в таком порядке как отдельные числа: верхняя левая горизонтальная координата, верхняя левая вертикальная координата, отдельно ширина и высота:

Rectangle RectangleArea = new Rectangle(0, 0, 50, 50)

Rectangle имеет достаточно много свойств чтения-записи для задания или извлечения его размеров в различных комбинациях:

Свойство

Описание

int Left

х-координата левого края

int Right

х-координата правого края

int Top

у-координата верхнего края

int Bottom

у-координата нижнего края

int X

То же самое что и Left

int Y

То же самое, что и Top

int Width

Ширина прямоугольника

int Height

Высота прямоугольника

Point Location

Верхний левый угол

Size Size

Размер прямоугольника

Отметим, что эти свойства не все независимы,— например задание Width будет влиять на значение Right.

Region

Мы упомянем здесь о существовании класса System.Drawing.Region, однако не будем рассматривать его подробно в этой книге. Region представляет область на экране, которая имеет некоторую сложную форму. Например, затененная область на рисунке может быть представлена Region:

Процесс инициализации экземпляра Region является сам по себе достаточно сложным. В целом это можно сделать, указывая, какие компоненты простой формы составляют область либо какой маршрут необходимо пройти, чтобы обойти область по границе. Если требуется работать с такими областями, то стоит изучить класс Region.

Замечание об отладке

Теперь мы готовы перейти к более сложным рисункам. Но сначала необходимо поговорить об отладке. Если попробовать задать точки прерывания для примеров в этой главе, то можно заметить, что отладка графических программ не является такой же простой задачей, как отладка других частей программы. Это связано с тем, что сам факт входа и выхода из отладчика часто приводит к отправке приложению сообщений Paint. В результате может оказаться, что задание точки прерывания в методе OnPaint заставляет приложение просто воспроизводить себя непрерывно, и поэтому оно не может делать ничего другого.

Типичный сценарий будет таков: необходимо определить, почему приложение что-то выводит неправильно, поэтому в OnPaint задается точка прерывания. Как ожидается, приложение доходит до точки прерывания и вызывает отладчик, в результате появляется окно среды разработки MDI.

В этом окне можно проверить значения некоторых переменных и даже найти что-нибудь полезное. Для продолжения работы нажмите клавишу F5, чтобы можно было увидеть, что происходит, когда приложение выводит что-то еще после выполнения некоторой обработки. К сожалению, в этот момент окно приложения выходит на передний план и Windows обнаруживает, что форма снова видна и немедленно посылает событие Paint. Это означает, конечно, что точка прерывания тут же сработает снова. Но обычно требуется, чтобы точка прерывания сработала позже, когда приложение нарисует что-то интересное, возможно, после выбора некоторых пунктов меню для чтения из файла или другого способа изменения изображения. Похоже на тупик. Либо не нужно вообще создавать точку прерывания в OnPaint, либо приложение никогда не сможет выйти за точку, где выводится его начальное окно.

Однако существуют способы обхода этой проблемы.

Если имеется достаточно большой экран, то простейшим способом является сохранение окна среды разработчика открытым, но так, чтобы оно не закрывало окно приложения. К сожалению, в большинстве случаев это не очень практичное решение, так как окно среды разработки будет слишком маленьким. Альтернативное решение, которое использует тот же самый принцип, состоит в том, что приложение должно объявить себя самым верхним приложением во время отладки. Это делается заданием свойства TopMost класса Form, что можно легко осуществить в методе InitializeComponent:

private void InitializeComponent() {

this.TopMost = true;

Это означает, что приложение никогда не будет закрыто другими окнами (только другими самыми верхними окнами). Оно всегда остается поверх других окон, даже когда другое приложение получает фокус. Так ведет себя менеджер задач.

Даже при использовании этой техники необходимо быть внимательным, так как никогда нет полной уверенности в том, что Windows не решит по какой-либо причине инициировать событие Paint. Если действительно в OnPaint требуется выявить некоторую проблему, возникающую при некоторых специальных условиях (например, приложение выполняет рисование после выбора определенного пункта в меню и что-то происходит в этом месте неправильно), то лучше всего поместить пустой код в OnPaint, который проверит некоторое условие, справедливое только в определенных обстоятельствах. А затем помещаем точку прерывания внутрь блока if следующим образом:

protected override void OnPaint(PaintEventArgs e) {

// Condition() оценивается как true, когда требуется прерывание

if (Condition() == true) {

int ii = 0; // <-- ЗАДАТЬ ЗДЕСЬ ТОЧКУ ПРЕРЫВАНИЯ

}

Это быстрый и простой способ создания условной точки прерывания.

Изображение прокручиваемых окон

Наш ранний пример DrawShapes работал хорошо, так как все, что нужно было нарисовать, поместилось в окне начального размера. В этом разделе мы обсудим, что необходимо сделать, если это не так.

Расширим пример DrawShapes для демонстрации прокрутки. Начнем с создания примера BigShapes, в котором сделаем прямоугольник и эллипс немного больше. При этом продемонстрируем, как использовать структуры Point, Size и Rectangle, используя их для определения областей рисования. С такими изменениями соответствующая часть класса Form1 выглядит следующим образом:

// поля-члены

private Point reсtangleTopLeft = new Point(0, 0);

private Size rectangleSize = new Size(200, 210);

private Point ellipseTopLeft = new Point(50, 200);

private Size ellipseSize = new Size(200, 150);

private Pen bluePen = new Pen(Color.Blue, 3);

private Pen redPen = new Pen(Color.Red, 2);

private void InitializeComponent() {

this.components = new System.ComponentModel.Container();

this.Size = new System.Drawing.Size(300, 300);

this.Text = "Scroll Shapes";

this.BackColor = Color.White;

}

#endregion

protected override void OnPaint(PaintEventArgs e) (

Graphics dc = e.Graphics;

if (e.ClipRectaringle.Top < 350 || e.ClipRectangle.Left < 250) {

Rectangle RectangleArea =

new Rectangle(RectangleTopLeft, RectangleSize);

Rectangle EllipseArea =

new Rectangle(EllipseTopLeft, EllipseSize);

dc.DrawRectangle(BluePen, RectangleArea);

dc.DrawEllipse(RedPen, EllipseArea);

}

base.OnPaint(e);

}

Отметим, что мы превратили объекты Pen в поля-члены — это более эффективно, чем создание нового объекта Pen каждый раз, когда нужно что-то нарисовать, как это делалось до сих пор.

Результат выполнения этого примера выглядит следующим образом:

Сразу можно увидеть проблему. Фигуры не вписываются в область рисования 300×300 пикселей.

Обычно, если документ является слишком большим для изображения, приложение добавляет панель прокрутки, чтобы позволить прокручивать окно и видеть выбранную часть изображения. Это еще одна область, где для интерфейса пользователя (см. главу 9) предоставляется среда времени выполнения .NET и базовым классам делать всю работу.

Если форма имеет различные элементы управления, присоединенные к ней, то экземпляр класса Form знает, где находятся эти элементы управления, и, следовательно, осведомлен, что если его окно становится слишком маленьким, то нужны панели прокрутки. Экземпляр Form будет также автоматически добавлять панели прокрутки и не только это, он способен также правильно нарисовать любую часть экрана, на которую произойдет перемещение. В этом случае нет ничего, что требуется делать в коде явно. Однако в этой главе мы берем ответственность за рисование на экране и поэтому собираемся помочь экземпляру Form, когда потребуется использовать прокрутку.

В последнем параграфе сказано: "если документ слишком большой для вывода". Это может привести к мысли, что речь идет о документе Word или Excel. Для приложений рисования, однако, лучше представлять себе документ как произвольные данные. Для текущего примера прямоугольник и эллипс составляют документ.

Добавление панелей прокрутки делается очень просто. Объект Form может по-прежнему обрабатывать все это для нас, причина, по которой так не делается в приведенном выше примере, состоит в том, что он не знает о необходимости этого, так как ему неизвестен размер области, в которой будет происходить рисование. Более точно, нам нужно знать размер прямоугольника, который простирается от верхнего левого угла документа (или, эквивалентно, верхнего левого угла клиентской области, прежде чем делается какая-либо прокрутка) и которая достаточно велика, чтобы содержать весь документ. В данной главе эта область будет называться областью документа. Взглянув на рисунок "документа", можно увидеть, что для нашего примера область документа составляет (250, 350) пикселей.

Сообщить форме размер документа достаточно просто. Мы используем соответствующее свойство Form.AutoScrollMinSize. Поэтому мы пишем следующий код:

private void InitializeComponent() {

this.components = new System.ComponentModel.Container();

this.Size = new System.Drawing.Size(300, 300);

this.Text = "Scroll Shapes";

this.BackColor = Color.White;

this.AutoScrollMinSize = new Size(250, 350);

}

Отметим, что здесь мы имеем MinScrollSize в методе InitializeComponent. Это удачный фрагмент в данном конкретном приложении, так как мы всегда знаем, каков будет размер экрана. Наш "документ" никогда не изменяет размер, пока выполняется это конкретное приложение. Помните, однако, что если приложение делает, например, вывод содержимого файлов или что-то еще, где область экрана может изменяться, то потребуется задание этого свойства в другое время.

Задания MinScrollSize для начала вполне достаточно. Давайте посмотрим, как теперь выглядит ScrollShapes. Мы имеем экран, который правильно выводит фигуры:

Отметим, что форма не только правильно задает панели прокрутки, но даже правильно масштабирует их, чтобы указать, какая часть документа выводится в данный момент. Если мы попробуем изменить размер окна во время выполнения примера, то окажется, что панели прокрутки реагируют правильно и даже исчезают, если сделать окно достаточно большим, так что необходимость в них отпадет.

Однако посмотрим теперь, что происходит, если действительно воспользоваться одной из панелей прокрутки и сместить немного изображение вниз:

Очевидно, что-то происходит неправильно.

Фактически неправильное поведение связано с тем, что не было принято во внимание положение панелей прокрутки в коде метода OnPaint(). Это легко увидеть, если заставить окно полностью перерисовать себя, минимизировав и затем восстановив его. Результат выглядит так:

Фигуры нарисованы, как и раньше, с верхним левым углом прямоугольника, помещенным в верхний левый угол клиентской области, как если бы панель прокрутки вообще не перемещалась.

Прежде чем перейти к решению этой проблемы, рассмотрим, что происходит на снимках экрана. Это поможет точно понять, как выполняется рисование в присутствии панелей прокрутки, и в то же время будет хорошей практикой. Если начать использовать GDI+, то рано или поздно встретится ситуация со странными рисунками, такая, как одна из приведенных выше, что потребует определить, что же происходит неправильно.

Посмотрим сначала на последний снимок экрана, с которым проще иметь дело. Пример ScrollShapes был только что восстановлен, поэтому все окно перерисовано. Взглянув на код, можно видеть, что он дает указание экземпляру Graphics нарисовать прямоугольник с верхними левыми координатами (0, 0) относительно верхнего левого угла клиентской области окна — то, что и было нарисовано. Проблема в том, что экземпляр Graphics по умолчанию интерпретирует координаты относительно клиентского окна, ведь он ничего не знает о панелях прокрутки. Код также не пытается настроить координаты в соответствии с позициями панелей прокрутки. То же самое происходит для эллипса.

Теперь посмотрим на более ранний снимок экрана — сразу после прокрутки изображения вниз. Мы замечаем, что здесь верхние две трети окна выглядят нормально. Это связано с тем, что они были нарисованы, когда приложение запускалось в первый раз. При прокрутке окна Windows не просит приложение перерисовать то, что уже было на экране. Система Windows достаточно разумна, чтобы самостоятельно определить, какие биты из изображаемых в данный момент на экране могут плавно переместиться, чтобы соответствовать текущему положению панели прокрутки. Это значительно более эффективный процесс, так как он может использовать некоторые аппаратные средства ускорения. Часть этого изображения экрана, которая выглядит неправильно, составляет нижнюю треть окна. Эта часть окна не была нарисована, когда приложение появилось на экране впервые, так как до начала прокрутки она находилась вне клиентской области. Значит, система Windows просит приложение ScrollShapes нарисовать эту область. Она инициирует событие Paint, передавая именно эту область в качестве прямоугольника вырезания. И именно это сделал метод OnPaint(). Такое довольно странное изображение экрана возникает в приложении, которое сделало в точности то, что ему было приказано.

Один из способов решения проблемы состоит в следующем. Мы в данный момент задаем координаты относительно верхнего левого угла начала "документа", а нам необходимо преобразовать их, чтобы задать относительно верхнего левого угла клиентской области. Рисунок должен это четко показать. На нем тонкие прямоугольники отмечают границы области экрана и всего документа (чтобы сделать рисунок понятнее, документ на самом деле расширен вниз и вправо за границы экрана, но это не изменяет рассуждения. Мы также предполагаем небольшую горизонтальную и вертикальную прокрутки). Толстые линии отмечают прямоугольник и эллипс, которые мы пытаемся нарисовать. Некоторая произвольно нарисованная точка Р будет служить в качестве примера. При вызове методов рисования мы предоставляем экземпляр объекта Graphics с вектором из точки В в точку Р, этот вектор представляет экземпляр Point. На самом деле нам нужен вектор из точки А в точку Р.

Проблема в том, что неизвестно, каким будет вектор из А в Р. Мы знаем, какой будет вектор из В в Р, это просто координаты Р относительно верхнего левого угла документа, позиция, где мы хотим нарисовать в документе точку Р. Мы знаем также, какой будет вектор из В в А — это величина, на которую была выполнена прокрутка; она хранится в свойстве класса Form с именем AutoScrollPosition. Однако мы не знаем вектор, направленный из А в Р. Чтобы найти искомый вектор, надо вычесть два вектора. Например, чтобы попасть из В в Р надо переместиться на 150 пикселей вправо и на 200 пикселей вниз, а чтобы попасть из B в А, необходимо переместиться на 10 пикселей вправо и на 57 пикселей вниз. Это означает, что для тою чтобы попасть из А в Р. необходимо переместиться на 140 (= 150 - 10) пикселей вправо и на 143 (= 200 - 57) пикселя вниз:

Однако все выполняется проще. Весь процесс был расписан подробно, чтобы показать, что происходит на самом деле, но класс Graphics на самом деле реализует метод, делающий эти вычисления. Он называется TranslateTransform. Ему передаются в качестве параметров горизонтальная и вертикальная координаты, которые указывают, где находится верхний левый угол клиентской области относительно верхнего левого угла документа (наше свойство AutoScrollPosition, которое определяет на рисунке вектор от В к А). Затем устройство Graphics с этого момента будет рассчитывать все координаты, принимая во внимание, где находится клиентская область относительно документа.

После всех этих объяснений осталось только добавить следующую строку кода в код рисования

dc.TranslateTransform(this.AutoScrollPosition.X, this.AutoScrollPosition.Y);

Фактически в нашем примере это происходит немного сложнее, так как мы по отдельности контролируем область вырезания, проверяя, нужно ли делать какое-либо рисование. Мы должны настроить эту проверку с учетом положения прокрутки, после чего весь код рисования для этого примера (загружаемый с web-сайта Wrox Press как ScrollShapes) выглядит таким образом:

protected override void OnPaint(PaintEventArgs e) {

Graphics dc = e.Graphics;

Size ScrollOffset = new Size(this.AutoScrollPosition);

if (e.ClipRectangle.Top + ScrollOffset.Width < 350 || e.ClipRectangle.Left + ScrollOffset.Height < 250) {

Rectangle RectangleArea =

new Rectangle(RectangleTopLeft + ScrollOffset, RectangleSize);

Rectangle EllipseArea =

new Rectangle(EllipseTopLeft + ScrollOffset, EllipseSize);

dc.DrawRectangle(BluePen, RectangleArea);

dc.DrawEllipse(RedPen, EllipseArea);

}

base.OnPaint();

}

Теперь код прокрутки работает прекрасно, мы можем, наконец, получить правильно прокрученный снимок экрана.

Координаты мировые, страницы и устройства

Различие в измерениях позиции относительно верхнего левого угла документа и относительно верхнего левого угла экрана является настолько значительным, что GDI+ имеет для них специальные названия.

□ Мировые координаты являются позицией точки, измеренной в пикселях от верхнего левого угла документа. Название отражает тот факт, что весь документ может рассматриваться как "мир" с точки зрения программы.

□ Координаты страницы являются позицией точки, измеренной в пикселях от верхнего левого угла клиентской области. Название идет от представления выводимой области как "страницы" выводимых данных.

Разработчики, знакомые с GDI, заметят, что мировые координаты соответствуют логическим координатам GDI. Координаты страницы соответствуют координатам устройства GDI. Эти разработчики должны также заметить, что способ кодирования преобразования между логическими координатами и координатами устройства в GDI+ изменился. В GDI преобразование осуществлялось через контекст устройства с помощью функций API Windows LPtoDP() и DPtoLP(). В GDI+ информацию, необходимую для выполнения преобразования, поддерживает объект Form.

GDI+ определяет также и третьи координаты, которые теперь называются координатами устройства. Координаты устройства аналогичны координатам страницы, за исключением того, что в качестве единиц измерения используются не пиксели, а другие единицы, которые пользователь может определить с помощью свойства Graphics.PageUnit. Возможные единицы измерения, помимо используемых по умолчанию пикселей, включают дюймы и миллиметры. Хотя свойство PageUnit в этой главе не будет использоваться, оно может быть полезно как способ обойти проблему различной плотности пикселей на устройствах. Например, 100 пикселей на большинстве мониторов будут занимать около дюйма. Однако лазерные принтеры могут иметь до тысяч dpi (точек на дюйм), что означает, что фигура в 100 пикселей шириной будут выглядеть значительно меньше при печати на таком лазерном принтере. Задавая единицы измерения, например, дюймы, и определяя, что фигура должна быть шириной в 1 дюйм, можно гарантировать, что фигура будет одного размера на различных устройствах.

Цвета

В этом разделе мы рассмотрим способы, с помощью которых можно определить цвет для рисования.

Цвета в GDI+ представлены экземплярами структуры System.Drawing.Color. Обычно после создания экземпляра такой структуры с ним почти ничего нельзя делать, только передавать в какой-либо вызываемый метод, требующий Color. Мы встречали эту структуру раньше, когда задавали фоновый цвет клиентской области окна в примерах. Свойство Form.BackColor в действительности возвращает экземпляр Color. Рассмотрим эту структуру более подробно. В частности, проверим несколько различных способов создания Color.

Значения красный-зеленый-синий (RGB)

Общее число цветов, которое можно изобразить на мониторе, огромно — более 16 млн. Точнее, оно равно 2 в 24-й степени, что составляет 16777216. Требуется некоторый способ индексирования этих цветов, чтобы можно было указать, какой цвет мы хотим использовать для данного пикселя.

Наиболее распространенный способ индексирования цветов состоит в разделении их на компоненты красного, зеленого и синего цветов. Эта идея основывается на принципе, что любой цвет, различаемый человеческим глазом, можно создать из некоторого количества красного, зеленого и синего. Эти цвета называются компонентами. На практике оказывается, что если разделить величину каждого компонента на 256 возможных интенсивностей, то это дает достаточно тонкую градацию, чтобы можно было вывести изображения, которые воспринимаются человеческим глазом как имеющие фотографическое качество. Поэтому мы определяем цвета, задавая значения этих компонентов на шкале от 0 до 255, где 0 означает, что компонент отсутствует, а 255 означает, что он имеет максимальную интенсивность.

Теперь можно понять, откуда появляется число 16777216, оно равно 256 в кубе.

Это дает нам первый способ задания цвета в GDI+. Можно указать значения для красного, синего и зеленого цветов, вызывая статическую функцию Color.FromArgb(). Компания Microsoft решила не поставлять конструктор для этой задачи. Причина в том, что существуют другие способы, помимо обычных компонентов RGB, для указания конструктора. В связи с этим Microsoft решила, что значения параметров, передаваемых в любой конструктор, будет подвержено неверной интерпретации:

Color RedColor = Color.FromArgb(255, 0, 0);

Color FunnyOrangyBrownColor = Color.FromArgb(255, 155, 100);

Color BlackColor = Color.FromArgb(0, 0, 0);

Color WhiteColor = Color.FromArgb(255, 255, 255);

Три параметра являются соответственно количествами красного, синего и зеленого цвета. Существует ряд других перегружаемых методов для этой функции, некоторые из них также позволяют определить так называемую альфа-смесь (отсюда буква А в названии метода FromArgb()!). Альфа-смешивание не рассматривается в этой главе, оно позволяет рисовать полупрозрачными тонами, комбинируя с цветом, который уже имеется на экране. Это может создавать красивые эффекты и часто используется в играх.

Именованные цвета

Создание структуры Color с помощью FromArgb() является наиболее гибкой техникой, так как она по сути означает, что можно определить любой цвет, который различает человеческий глаз. Однако, если требуется простой, стандартный, хорошо известный цвет, такой как красный или синий, то значительно легче просто назвать требуемый цвет. В связи с этим Microsoft предоставляет также большое число статических свойств в Color, каждое из которых возвращает именованный цвет. Одно из этих свойств использовалось, когда в примерах задавался фоновый цвет окон как белый:

this.BackColor = Color.White;

// имеет такой же эффект, как и

// this.BackColor = Color.FromArgb(255, 255, 255);

Существует несколько сотен таких цветов. Полный список дан в документации MSDN. Он включает все простые цвета: Red, White, Blue, Green, Black и т.д., а также такие, как MediumAquamarine, LightCoral и DarkOrchid.

В связи с этим такие именованные цвета были выбраны не случайно. Каждый из них представляет определенный набор значений RGB, и они были первоначально выбраны много лет назад для использования в Интернете. Идея состояла в том, чтобы предоставить полезный набор цветов по всему спектру, имена которых будут распознаваться браузерами Web, таким образом позволяя избежать написания явных значений RGB в коде HTML. Несколько лет назад эти цвета были очень важны, так как ранние браузеры не могли точно воспроизводить многие цвета и именованные цвета, правильно выводимые большинством браузеров, предназначались для обеспечения множества цветов. Сегодня этот аспект не имеет решающего значения, так как современные браузеры Web вполне способны вывести правильно любое значение RGB.

Режимы вывода графики и палитра безопасности

При том что в принципе мониторы могут вывести любой из 16 млн цветов RGB, на практике это зависит от того, как заданы свойства вывода изображения на компьютере. Делая щелчок правой кнопкой мыши на рабочем столе Windows и выбирая Settings в появляющейся таблице свойств, можно получить цветовое разрешение изображения. Здесь традиционно существует три основных варианта (хотя некоторые машины могут предоставлять другие возможности в зависимости от оборудования): true color (24-битовые), high color (16-битовые) и 256 цветов. (На некоторых графических платах сегодня true color в действительности помечены как 32-битовые с целью оптимизации аппаратного обеспечения, хотя в этом случае для самого цвета используются только 24 бита из 32).

Только режим true color позволяет выводить одновременно все цвета RGB. Это лучший вариант, но требует дополнительных расходов: для хранения полного значения RGB требуется 3 байта, т. е. для хранения каждого выводимого пикселя требуется 3 байта памяти графической платы. Если памяти графической платы достаточно (ограничение, которое встречается сегодня реже), то можно выбрать такой режим. Режим high color использует два байта на пиксель. Этого достаточно, чтобы задать 5 битов для каждой компоненты RGB. Поэтому вместо 256 градаций интенсивности красного, получается только 32 градации; то же самое для синего и зеленого, что дает всего 65536 цветов. Этого вполне достаточно, чтобы получить почти фотографическое качество при поверхностном рассмотрении, хотя области с легким затенением покажутся слегка неровными.

256-цветовой режим дает еще меньше цветов. Однако в этом режиме можно выбрать используемые цвета. В системе задается так называемая палитра. Это список 256 цветов, выбранных из 16 миллионов цветов RGB. После задания цветов в палитре графическое устройство может выводить только эти цвета. Палитра изменяется в любое время, но графическое устройство по-прежнему выведет только 256 различных цветов в данный момент времени. 256-цветный режим используется в действительности только, когда необходимо получить высокую производительность и при небольшом объеме видеопамяти. Большинство игр будут использовать этот режим, но за счет тщательного выбора палитры они по-прежнему смогут предоставить хорошее графическое оформление.

Вообще, если устройство вывода находится в режиме high color или 256 цветов и запрашивается для вывода определенного цвета RGB, то оно будет выбирать ближайшее математическое соответствие из пула доступных цветов. По этой причине важно знать о режимах цветов. При рисовании объекта, который содержит слабые затенения или имеет фотографическое качество, и если не выбран режим 24-битовых цветов, пользователь может не увидеть изображения в том виде, как это должно быть. Если работа такого рода делается с помощью GDI+, необходимо проверить приложение в различных режимах цветов. (Приложение может также программным путем задать цветовой режим, хотя этот вопрос здесь рассматриваться не будет).

Палитра безопасности

Для справки мы кратко упомянем здесь палитру безопасности. Это обычно палитра, используемая по умолчанию. Она работает так, что для каждого цветового компонента задается шесть расположенных на одинаковом расстоянии друг от друга возможных значений. А именно, значения 0, 51, 102, 153, 204, 255. Другими словами, красный компонент может иметь любое из этих значений. То же самое можно сказать о зеленом и синем компонентах. Поэтому возможные цвета из палитры безопасности включают (0, 0, 0) (черный), (153, 0, 0) (достаточно темный оттенок красного), (0, 255, 102) (зеленый с небольшой голубизной) и т. д. Это дает всего 6 в кубе = 216 цветов. Идея состоит в том, что это дает нам простой способ иметь палитру, которая содержит цвета из всего спектра и всех степеней яркости, хотя на практике это работает не так хорошо, так как равное математическое разделение цветовых компонентов не значит равного восприятия различия цветов человеческим глазом. Но поскольку палитра безопасности широко используется, можно найти большое число приложений и изображений, которые используют цвета исключительно из палитры безопасности.

При использовании 256-цветного режима Windows палитрой по умолчанию является палитра безопасности с добавленными 20 стандартными цветами Windows и 20 свободными цветами.

Перья и кисти

В этом разделе мы сделаем обзор двух вспомогательных классов, которые нужны для рисования фигур. Мы уже встречали класс Pen, используемый для сообщения экземпляру Graphics, как рисовать линии. Связанным является класс System.Drawing.Brush, который говорит, как заполнять области. Например, Pen требуется для рисования контуров прямоугольников и эллипсов в рассмотренных ранее примерах. Если понадобится нарисовать эти фигуры как заполненные, то для этого должна использоваться кисть, которая определяет, как заполнять фигуру. Одной из особенностей этих двух классов является то, что на них вряд-ли когда-нибудь будут вызываться какие-либо методы. Обычно просто создается экземпляр Pen или Brush с требуемым цветом и другими свойствами, а затем он передается в методы рисования.

Рассмотрим сначала кисти, а затем — перья.

Программисты, использовавшие ранее GDI, могут заметить из первых примеров, что перья используются в GDI+ другим способом. В GDI обычная практика состояла в вызове функции API Windows с именем SelectObject(), которая обычно связывает перо с контекстом устройства. Оно используется затем во всех операциях рисования, пока контекст устройства не будет связан с другим пером, снова вызывая метод SelectObject(). Тот же принцип сохраняется для кистей и других объектов, таких как шрифты или битовые изображения. С помощью GDI+, как упоминалось ранее, компания Microsoft перешла к модели без состояния, в которой нет пера по умолчанию или другого вспомогательного объекта. Вместо этого с каждым вызовом метода просто определяется подходящий вспомогательный объект, который будет использоваться для определенного метода.

Кисти

GDI+ имеет несколько различных видов кистей, мы объясним простейшие из них, чтобы знать о принципах. Каждый тип кисти представлен экземпляром класса, производным из System.Drawing.Brush (этот класс является абстрактным, поэтому нельзя создать экземпляры объектов Brush как только объекты производных классов). Простейшая кисть указывает, что область должна быть заполнена сплошным цветом. Этот вид кисти представлен экземпляром класса System.Drawing.SolidBrush, который можно создать таким образом:

Brush solidBeigeBrush = new SolidBrush(Color.Beige);

Brush solidFunnyOrangyBrownBrush = new SolidBrush(Color.FromArgb(255, 155, 100)

Альтернативно, если кисть является одним из именованных цветов Интернета, то можно создать кисть более просто с помощью другого класса System.Drawing.Brushes. Brushes является одним из тех классов, экземпляры которых реально никогда не создаются (он имеет закрытый конструктор, чтобы не дать возможности это сделать). Большое число статических свойств возвращает кисть специального цвета. Brushes используется так:

Brush solidAzureBrush = Brushes.Azure;

Brush solidChocolateBrush = Brushes.Chocolate;

Следующий уровень сложности представляет штриховая кисть, которая заполняет область, рисуя некоторый шаблон-узор. Этот тип кисти находится в пространстве имен Drawing2D, представленном классом System.Drawing.Drawing2D.HatchBrush. Класс Brushes не сможет помочь в случае штриховой кисти, необходимо будет создать одну из них явно, задавая стиль штриховки и два цвета — цвет переднего плана и цвет фона (но можно опустить цвет фона, в таком случае по умолчанию используется черный цвет). Стиль штриховки задают с помощью перечисления System.Drawing.Drawing2D.HatchStyle. Существует большое число доступных значений HatchStyle, поэтому проще всего обратиться к документации MSDN для получения полного списка. Типичными стилями, например, являются ForwardDiagonal, Cross, DiagonalCross, SmallConfetti и ZigZag. Ниже приведены примеры создания штриховой кисти

Brush crossBrush = new HatchBrush(HatchStyle.Cross, Color.Azure);

// фоновый цвет для CrosstBrush будет черный

Brush brickBrush = new HatchBrush(HatchStyle.DiagonalBrick, Color.DarkGoldenrod.Color.Cyan);

Сплошные и штриховые кисти — единственные кисти, доступные в GDI. GDI+ добавляет пару новых стилей кисти:

□ Кисть System.Drawing.Drawing2D.LinearGradientBrush заполняет область цветом, который изменяется на экране.

□ Кисть System.Drawing.Drawmg2D.PathGradientBrush действует аналогично, но в этом случае цвет меняется вдоль кривой в закрашиваемой области. Мы не будем рассматривать здесь эти кисти. Отметим только, что обе они могут создать интересные эффекты при аккуратном использовании. Пример Bezier из главы 9 использует кисть с линейным градиентом для закрашивания фона окна.

Перья

В отличие от кистей перья представлены только одним классов — System.Drawing.Pen. Перо в действительности является немного более с южным, чем чисть, так как оно должно указывать толщину линий (ширину в пикселях), а для широкой линии,— как закрашивать область внутри нее. Перья могут также определять ряд других свойств, которые находятся за пределами нашего рассмотрения, но включают упоминавшееся ранее свойство Alignment, указывающее, где относительно границы фигуры должна быть нарисована линия, а также какую фигуру нарисовать в конце линии (нужно ли округлять форму).

Область внутри толстой линии может быть закрашена сплошным цветом с помощью пера или кисти. Следовательно, экземпляр Pen может содержать ссылку на экземпляр Brush. Это достаточно мощное средство, так как при его содействии можно нарисовать линии, окрашенные с помощью штрихования или линейного затенения. Существует четыре различных способа, с помощью которых создаются экземпляры Pen. Можно задать цвет или кисть, оба эти конструктора будут создавать перо шириной в один пиксель. Можно также в дополнение к цвету или кисти задать значение типа float, представляющее ширину пера (на тот случай, если для объекта Graphics, который будет выполнять рисование, используются нестандартные единицы измерения, такие как миллиметры или дюймы, или доли дюймов). Поэтому, например, можно так создавать перья:

Brush brickBrush = new HatchBrush(HatchStyle.DiagonalBrick, Color.DarkGoldenrod, Color.Cyan);

Pen solidBluePen = new Pen(Color.FromArgb(0, 0, 255));

Pen solidWideBluePen = new Pen(Color.Blue, 4);

Pen brickPen = new Pen(BrickBrush);

Pen brickWidePen = new Pen(BrickBrush, 10);

Кроме того, для быстрого создания перьев можно использовать класс System.Drawing.Pens, который подобно классу Brushes содержит ряд готовых перьев. Эти перья все имеют ширину в один пиксель и используют обычное множество именованных цветов Интернета, что позволяет создавать перья таким образом:

Pen SolidYellowPen = Pens.Yellow;

Рисование фигур и линий

В первой части главы были рассмотрены базовые классы и объекты, требуемые для рисования специальных фигур и т.д. на экране. Теперь дадим обзор некоторых методов рисования, предоставляемых классом Graphics, и в конце — кратким примером проиллюстрируем несколько кистей и перьев.

System.Drawing.Graphics имеет большое число методов, позволяющих рисовать различные линии, контуры фигур и сплошные фигуры. Их существует слишком много, но таблица (см. ниже) представляет основные методы и дает представление о множестве фигур, которые можно нарисовать.

Метод

Типичные параметры

Что рисует

DrawLine

Перо, начальная и конечная точки

Одиночная прямая линия

DrawRectangle

Перо, позиция и размер

Контур прямоугольника

DrawEllipse

Перо, позиция и размер

Контур эллипса

FillRectangle

Кисть, позиция и размер

Закрашенный прямоугольник

FillEllipse

Кисть, позиция и размер

Закрашенный эллипс

DrawLines

Перо, массив точек

Последовательность линий, соединяющих каждую точку в массиве со следующей

DrawBezier

Перо, 4 точки

Гладкая кривая, соединяющая две конечные точки и проходящая через две оставшиеся точки, используемые для управления формой кривой

DrawCurve

Перо, массив точек

Гладкая кривая, проходящая через точки

DrawArc

Перо, прямоугольник, два угла

Часть окружности внутри прямоугольника, определенная углами

DrawClosedCurve

Перо, массив точек

Подобен DrawCurve, но рисует также прямую линию для соединения концов кривой

DrawPie

Перо, прямоугольник, два угла

Клиновидный контур внутри прямоугольника

FillPie

Кисть, прямоугольник, два угла

Закрашенная клиновидная область в прямоугольнике

DrawPolygon

Перо, массив точек

Подобен DrawLines, но соединяет также первую и последнюю точки для замыкания нарисованной фигуры

Прежде чем закончить тему рисования простых объектов, создадим пример, который демонстрирует разновидности визуальных эффектов, создаваемых с помощью кистей. Пример называется ScrollMoreShapes и является по сути пересмотром примера ScrollShapes. Помимо прямоугольника и эллипса, добавим толстую линию и закрасим фигуры с помощью различных кистей. Мы уже объясняли принципы рисования, поэтому код представлен с минимальными комментариями. Первое: в связи с новыми кистями, нам нужно указать, что используется пространство имен System.Drawing.Drawing2D:

using System;

using System.Drawing;

using System.Drawing.Drawing2D;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

Используем несколько дополнительных полей в классе Form1, которые содержат данные о местах, где должны быть нарисованы фигуры, а также различные перья и кисти:

private Rectangle rectangleBounds =

new Rectangle(new Point(0, 0), new Size(200, 200));

private Rectangle ellipseBounds =

new Rectangle(new Point(50, 200), new Size(200, 150));

private Pen BluePen = new Pen(Color.Blue, 3);

private Pen RedPen = new Pen(Color.Red, 2);

private Brush SolidAzureBrush = Brushes.Azure;

private Brush CrossBrush = new HatchBrush(HatchStyle.Cross, Color.Azure);

static private Brush BrickBrush =

new HatchBrush(HatchStyle.DiagonalBrick, Color.DarkGoldenrod, Color.Cyan);

private Pen BrickWidePen = new Pen(BrickBrush, 10);

Поле BrickBrush объявлено как статическое, чтобы использовать его значение в инициализаторе BrickWidePen, который далее следует. C# не позволит использовать поле одного экземпляра объекта для инициализации поля другого экземпляра, так как не определено, какое из них будет инициализировано первым. Но объявление поля как static решает проблему, так как создается только один экземпляр класса Form1, поэтому неважно, будут ли поля статическими или полями экземпляра. Вот метод OnPaint():

protected override void OnPaint(PaintEventArgs e ) {

Graphics dc = e.Graphics;

Point scrollOffset = this.AutoScrollPosition;

dc.TranslateTransform(scrollOffset.X, scrollOffset.Y);

if (e.ClipRectangle.Top+scrollOffset-X < 350 ||

e.ClipRectangle.Left+scrollOffset.Y < 250) {

dc.DrawRectangle(BluePen, rectangleBounds);

dc.FillRectangle(CrossBrush, rectangleBounds);

dc.DrawEllipse(RedPen, ellipseBounds);

dc.FillEllipse(SolidAzureBrush, ellipseBounds);

dc.DrawLine(BrickWidePen, rectangleBounds.Location,

ellipseBounds.Location + ellipseBounds.Size);

}

base.OnPaint(e);

}

А это результат:

Отметим, что толстая диагональная линия лежит поверх прямоугольника и эллипса, так как это был последний нарисованный элемент.

Вывод изображений

Одним из наиболее распространенных действий, которое может понадобиться сделать с помощью GDI+, является вывод изображений, уже существующих в файле. Это значительно проще, чем рисование своего собственного интерфейса пользователя, так как изображение уже было нарисовано. По сути необходимо только загрузить файл и приказать GDI+ вывести его. Изображение может быть простым графическим рисунком, пиктограммой или сложным изображением, таким как фотография. Можно выполнить некоторые манипуляции с изображением, такие как растягивание и вращение, или вывести только его часть.

В данном разделе представим пример, затем обсудим несколько вопросов, о которых необходимо знать при выводе изображений. Мы можем сделать это, так как код для вывода изображений очень прост.

Image MyImage = Image.FromFile("FileName"!);

FromFile() является статическим членом класса Image и обычным способом создает экземпляр изображения. Файл может быть любым из обычно поддерживаемых форматов графических файлов, включая .bmp, .jpg, .gif и .png.

Вывод изображения требует также только одну строку кода в предположении, что имеется подходящий экземпляр объекта Graphics:

dc.DrawImageUnscaled(MyImage, TopLeft);

В этой строке кода dc предполагается экземпляром объекта Graphics, MyImage является Image, который будет выведен, a TopLeft — структурой Point, которая хранит координаты устройства, где требуется поместить изображение. Трудно представить себе что-то более простое.

По всей вероятности, изображения являются областью, в которой разработчики знакомые с GDI, заметят наибольшие различия с GDI+. В GDI работа с изображениями была достаточно непредсказуемой. Вывод изображения включал несколько нетривиальных шагов. Если изображение задавалось как битовое, загрузка его была относительно простой, но загрузка любого другого типа файла включала последовательность вызовов объектов OLE. В действительности вывод загруженного изображения на экран включал получение для него дескриптора, выбор его в памяти контекста устройства и затем выполнение блочного переноса между контекстами устройств. Хотя контексты устройств и дескрипторы, по-прежнему находятся за сценой и понадобятся, если придется делать (ложное редактирование изображений в коде программы, простые задачи теперь погружены в объектную модель GDI+.

Мы проиллюстрируем процесс вывода изображения с помощью примера DisplayImage. Этот пример просто выводит файл .jpg в основном окне приложения. Чтобы упростить код, путь доступа файла .jpg жестко закодирован в приложении (поэтому при выполнении приложения необходимо изменить его в соответствии с местоположением файла на используемой системе). Выводимый файл .jpg является групповой фотографией участников встречи COMFest.

Как обычно в этой главе, проект DisplayImage является стандартным приложением Windows, созданным с помощью VisualStudio.NET. Мы добавляем следующее поле в класс Form1:

Image Piccy;

Затем загружаем файл в процедуру InitializeComponent.

private void InitializeComponent() {

this.components = new System.ComponentModel.Container();

this.Size = new System.Drawing.Size(600, 400);

this.Text = "Display COMFEst Image";

this.BackColor = Color.White;

Piccy =

Image.FromFile(@"C:\ProCSharp\Chapter21\Display Image\CF4Group.jpg");

this.AutoScrollMinSize = Piccy.Size;

}

Отметим, что размер изображения в пикселях задается как его свойство Size, которое используется для задания области документа. Изображение выводится в методе OnPaint():

protected override void OnPaint(PaintEventArgs e) {

Graphics dc = e.Graphics;

dc.DrawImageUnscaled(Piccy, this.AutoScrollPosition);

base.OnPaint(e);

}

Выбор this.AutoScrollPosition в качестве координатного устройства гарантирует, что окно будет прокручиваться правильно, при этом до начала прокручивания изображение будет располагаться с верхнего левого угла клиентской области.

Наконец, сделаем еще одно замечание об изменениях, сделанных в коде метода Form1.Dispose(), созданном мастером:

public override void Dispose() {

base.Dispose();

if (components != null) components.Dispose();

Piccy.Dispose();

}

Удаление изображения, когда оно не требуется, является важной задачей, так как изображения обычно требуют много памяти. После вызова Image.Dispose() экземпляр Image больше не ссылается на какое-либо реальное изображение и поэтому не может больше выводиться (если не будет загружено новое изображение).

Выполнение этого кода создает результат:

COMFest (www.comfest.co.uk) является неформальной группой разработчиков в Великобритании, которые встречаются для обсуждения самых новых технологий, обмена идеями и т. д. Снимок включает всех участников COMFest 4, за исключением автора этой главы, который фотографировал.

Вопросы, возникающие при манипуляциях с изображениями

Вывод изображений выполнить легко, но все же требуется некоторое понимание описанной ниже технологии.

Наиболее важным моментом при работе с изображениями является то, что они всегда прямоугольные. Это не просто удобство для людей. Такая форма связана с тем, что все современные графические платы имеют встроенное оборудование, которое может очень эффективно копировать блоки пикселей из одного участка памяти в другой. При условии, что блок пикселей представляет прямоугольную область, аппаратное ускорение выполняется практически как одна операция, и поэтому будет очень быстрым. На самом деле это ключ к современной высокопроизводительной графике. Такая операция называется переносом битовых блоков (BitBlt). Image.DrawImageUnscaled() внутренне использует BitBlt, вот почему можно видеть огромное изображение, содержащее, возможно, миллионы пикселей (фотография из примера имеет 104975 пикселей) появляющимся почти мгновенно. Если бы компьютер должен был копировать изображение на экран пиксель за пикселем, то изображение постепенно появлялось бы в течение нескольких секунд.

Метод BitBlt очень эффективен, поэтому почти все операции рисования и манипуляции с изображениями выполняются с его помощью. Даже некоторое редактирование изображений будет делаться с помощью BitBlt, перенося части изображений между контекстами устройств, которые представляют области памяти. При использовании GDI функция BitBlt() из API Windows 32 была, наверное, самой важной и широко используемой функцией для манипуляции изображениями, хотя в GDI+ операции BitBlt по большей части скрыты объектной моделью GDI+.

Невозможно использовать BitBlt для областей, которые не являются прямоугольными, что можно легко смоделировать. Один из способов состоит в пометке некоторого цвета как прозрачного для целей BitBlt поэтому данная область цвета в изображении-источнике не будет перезаписывать существующий цвет соответствующего пикселя на получающем устройстве. Можно также определить, что в процессе выполнения BitBlt каждый пиксель получающегося изображения будет сформирован перед BitBlt некоторой логической операцией (такой, как побитовое AND) на цветах этого пикселя в изображении-источнике и в получающем устройстве. Такие операции поддерживаются аппаратным ускорителем и могут использоваться для задания ряда тонких эффектов. Не рассматривая детали процесса, отметим что объект Graphics реализует другой метод DrawImage(). Он аналогичен методу DrawImageUnscaled(), но поставляется с большим числом перезагружаемых версий, которые позволяют определить более сложные формы BitBlt для использования в процессе рисования. DrawImage() позволяет также рисовать (BitBlt) только определенную часта изображения, или выполнить на нем некоторые другие операции, такие как масштабирование (увеличение или уменьшение размера) при его рисовании.

Рисование текста

Мы отложили рисование текста на экране до этого момента, так как эта очень важная тема является в целом более сложной задачей, чем рисование графики. Необходимо уточнить это утверждение. Вывод одной или двух строк текста без учета их вида является очень простым, требуя вызова одного из методов экземпляра Graphics, Graphics.DrawString(). Однако при попытке вывести документ, содержащий большое количество текста, быстро обнаруживается значительное усложнение процесса. Это обусловлено двумя причинами:

□ Первое: для получения аккуратного изображения необходимо понимать шрифты. Там, где для рисования фигуры используются кисти и перья в качестве вспомогательных объектов, процесс рисования текста требует соответственно шрифтов в качестве вспомогательных объектов. Понимание шрифтов является не простой задачей. В следующем разделе будет предоставлено краткое введение в предмет. Заметим, однако, что детали шрифтов являются более сложными, чем кистей и перьев.

□ Второе: текст необходимо очень аккуратно разместить в окне. Пользователя обычно ожидают слова, которые естественно следуют одно за другим выровненными с пробелами между словами. Сделать это труднее, чем кажется. Обычно заранее неизвестно, сколько места на экране потребуется для слова (в отличие от фигуры). Это должно вычисляться (не беспокойтесь, это не придется делать вручную, так как существует метод Graphics.MeasureString(), который это сделает). Также пространство занимаемое словом на экране, будет влиять на местоположение каждого последующего слова документа. Если приложение выполняет перенос строк, то ему придется тщательно оценивать размеры слов, прежде чем решить, где поместить разрыв. Посмотрев внимательно на работу Word for Windows, можно заметить, что Word непрерывно изменяет положение текста при вводе, изменении шрифта, вырезании, вставке, и т. д. При этом выполняется большая обработка, включающая некоторые очень тщательно отработанные алгоритмы. Конечно, проектируемое приложение GDI+ не обязательно должно быть таким же сложным, как Word, но если потребуется вывести произвольный текст, то многие из таких же рассмотрений будут по-прежнему в силе. Поэтому конечная часть этой главы посвящена примеру, допускающему простые манипуляции с текстом, чтобы дать некоторое представление о проблемах, которые возникают в подобных приложениях, и об их возможных решениях.

Обработка текста с хорошим качеством не является невозможной, она просто сложна для правильного выполнения. Как было упомянуто ранее, реальный процесс вывода строки текста на экран при условии знания шрифта и места вывода трудности не представляет. Поэтому далее будет дан краткий пример, показывающий, как выводить фрагменты текста. В оставшейся части главы вы найдете обзор некоторых принципов Fonts и Font Families, прежде чем мы перейдем к более реалистичному примеру обработки текста CapsEditor, который продемонстрирует аспекты размещения текста на экране, а также покажет, как обрабатывать пользовательский ввод.

Простой пример с текстом

Пример является обычным результатом работы Windows Forms. В этот раз метод OnPaint() переопределяется следующим образом:

protected override void OnPaint(PaintEventArgs e) {

Graphics dc = e.Graphics;

Brush blackBrush = Brushes.Black;

Brush blueBrush = Brushes.Blue;

Font haettenschweilerFont = new Font("Haettenschweiler", 12);

Font boldTimesFont = new Font("Times New Roman", 10, FontStyle.Bold);

Font italicCourierFont = new Font("Courier", 11, FontStyle.Italic | FontStyle.Underline);

dc.DrawString("This is a groovy string", haettenschweilerFont, blackBrush, 10, 10);

c.DrawString("This is a groovy string " +

"with some very long text that will never fit in the box", boldTimesFont, blueBrush,

new Rectangle(new Point(10, 40), new Size(100, 40)));

dc.DrawString("This is a groovy string", italicCourierFont, blackBrush,

new Point(10, 100)); base.OnPaint(e);

}

Выполнение этого примера создает вывод:

Этот пример демонстрирует использование метода Graphics.DrawString() для рисования элементов текста. Существует несколько перезагружаемых версий DrawString(), из которых показаны три. Все различные версии требуют параметры, указывающие выводимый текст, шрифт для рисования текста и кисть, которая должна использоваться для создания различных линий и кривых, составляющих символы текста. Для оставшихся параметров существуют альтернативы. В целом, однако, можно определить либо Point (или эквивалентно два числа), либо Rectangle. Если определяется Point, то текст начнется своим верхним левым углом в этой точке Point и развернется вправо. Если определить Rectangle, то экземпляр Graphics поместит строку внутри этого прямоугольника. Если текст не впишется в границы прямоугольника, то он будет обрезан, как видно на снимке экрана. Передача прямоугольника в DrawString() означает, что процесс рисования продолжится дольше, так как DrawString() необходимо определить, где поместить разрывы строки, но результат может выглядеть лучше (если строка вписывается в прямоугольник).

Этот пример показывает также способы создания шрифтов. Всегда требуется имя шрифта и его размер (высота). Можно также при желании передать различные стили, изменяющие вид текста (жирный, подчеркивание и т. д.).

Шрифты и семейства шрифтов

Все интуитивно считают, что достаточно хорошо понимают шрифты. В конце концов мы видим их почти постоянно. Шрифт точно описывает, как должна выводиться каждая буква. Выбор подходящего шрифта, а также предоставление разумного множества шрифтов в документе является важным фактором улучшения читабельности документа. Можно просто взглянуть на страницы этой книги, чтобы увидеть, сколько использовалось шрифтов. Нужно тщательно выбирать шрифты, поскольку плохой выбор шрифта может существенно повредить как привлекательности, так и использованию приложения. Многие при вопросе о названии шрифта скажут что-нибудь типа 'Arial' или 'Times New Roman' или 'Courier'. Фактически это не шрифты вообще, это семейства шрифтов. Шрифты будут чем-нибудь типа Arial 9-point italic. Семейство шрифтов сообщает в общих терминах визуальный стиль текста и является ключевым фактором в общем представлении приложения. Большинство людей способны распознавать стили наиболее распространенных семейств шрифтов, даже если и не осознают этого. Шрифт добавляет дополнительную информацию, определяя размер текста, а также применение к тексту определенных модификаций. Например, будет ли это жирный, курсив или подчеркнутый текст, выведется ли он ЗАГЛАВНЫМИ БУКВАМИ или как подстрочный индекс. Такие модификации технически называются стилями, хотя в некотором смысле термин вводит в заблуждение, поскольку только что было отмечено, что визуальное представление определяется в основном семейством шрифта.

Размер текста определяется его высотой. Высота измеряется в пунктах (points) — традиционная единица измерения, которая представляет 1/72 дюйма (для людей, живущих за пределами Великобритании и США, пункт равен 0.351 мм). Поэтому, например, буквы в 10-пунктовом шрифте имеют в высоту 10/72 дюйма (или приблизительно 1/7", или 3.5 мм). Из этого объяснения может показаться, что семь строк текста с размером шрифта 10 разместятся по вертикали на одном дюйме экрана или пространства бумаги. Фактически их будет немного меньше, так как необходимо также обеспечить интервалы между строками.

Строго говоря, измерение высоты не так просто, как может показаться, поскольку есть несколько различных высот, которые необходимо учитывать. Например, существует высота высокой буквы, такой как А или f (этот размер мы действительно имеем в виду, когда говорим о высоте), дополнительная высота, занимаемая любым акцентом над буквой типа Å или Ń (внутренняя высота строки), и дополнительная высота ниже базовой линии, необходимая для нижних частей букв типа у или g (спуск). Однако для данной главы это несущественно. Если определено семейство шрифтов и основная высота, такие дополнительные высоты задаются автоматически — нельзя независимо выбрать их значения.

Иногда при работе со шрифтами могут встретиться и другие термины, используемые для описания некоторых семейств шрифтов.

□ Семейство шрифтов serif имеет небольшие черточки на концах многих линий, которые составляют символы. Эти черточки называют засечками (serifs). Times New Roman является классическим примером такого шрифта.

□ Семейства шрифтов sans serif, в противоположность, не имеют этих черточек. Хорошими примерами шрифтов sans serif являются Arial и Verdana. Отсутствие черточек часто придает тексту резкий, бросающийся в глаза вид, поэтому шрифты sans serif часто используются для выделения важного текста.

□ В семействе шрифтов true type очертания кривых, формирующих символы, вычисляются точными математическими способами. Это означает, что одно и то же определение может использоваться для вычисления того, как нарисовать шрифты семейства любого размера. Сегодня практически все используемые шрифты являются шрифтами true type. Некоторые старые семейства шрифтов из Windows 3.1 определялись с помощью отдельного битового изображения для каждого символа любого размера, но сегодня эти шрифты не используются. (Среди других недостатков они вызывают проблемы при переходе от экрана к современному принтеру, где число пикселей на дюйм значительно больше, поэтому битовые изображения будут выглядеть слишком маленькими).

Microsoft предоставляет два основных класса, с которыми необходимо иметь дело при выборе или манипуляциях со шрифтами. Это классы System.Drawing.Font и System.DrawingFontFamily. Мы уже видели основное использование класса Font. Когда необходимо нарисовать текст, создается экземпляр класса Font и передается методу DrawString для указания того, как должен быть нарисован текст. Экземпляр FontFamily используется для представления семейств шрифтов.

Одно из применений класса FontFamily возникает в ситуации, когда требуется шрифт определенного типа (Serif, SansSerif или Monospace), но неважно какой. Статические свойства GenericSerif, GenericSansSerif и GenericMonospace выявляют используемые по умолчанию шрифты, соответствующие этим критериям:

FontFamily sansSerifFont = FontFamily.GenericSansSerif;

Однако при написании профессионального приложения желательно выбирать шрифт более тщательно. Скорее всего, код рисования реализуется таким образом, что будет проверять, какие семейства шрифтов реально установлены на компьютере и, следовательно, какие шрифты доступны. Затем приложение выберет подходящий шрифт, возможно, взяв первый доступный шрифт из списка предпочтительных шрифтов. Если желательно, чтобы приложение имело более дружественный пользовательский интерфейс, то первым вариантом в списке предпочтительных будет шрифт, который пользователь выбирал в последний раз при выполнении этого приложения. Обычно используются наиболее популярные семейства шрифтов, такие как Arial и Times New Roman. Но если попробовать вывести текст с помощью шрифта, который не существует, результаты не всегда будут предсказуемы, и, скорее всего, Windows просто подставит стандартный системный шрифт, который очень легко нарисовать системе, но который выглядит не совсем удачно и, появившись в документе, создаст впечатление программного обеспечения плохого качества.

Шрифты, доступные в системе, можно определить с помощью класса, называемого InstalledFontCollection, который находится в пространстве имен System.Drawing.Text. Этот класс представляет реализации свойства Families, которое является массивом всех шрифтов, доступных для использования в системе:

InstalledFontCollection insFonc = new InstalledFontCollection();

FontFamily [] families = insFont.Families;

foreach (FontFamily family in families) {

// выполнить обработку с помощью этого семейства шрифтов

}

Пример: перечисление семейств шрифтов

В этом разделе мы разберем небольшой пример EnumFontFamilies, который перечисляет все семейства шрифтов, доступные в системе, и проиллюстрирует их, выводя имя каждого семейства с помощью подходящего шрифта (10-пунктовой обычной версии семейства этого шрифта). Когда пример выполняется, он выглядит так:

Отметим, однако, что в зависимости от установленных на компьютере шрифтов можно получить различные результаты. Для этого примера, как обычно, создается стандартное приложение C# для Windows с именем EnumFontFamilies. Затем мы добавляем следующую константу в класс Form1:

const int margin = 10;

margin является размером левого и верхнего полей между текстом и краем документа, он не позволяет тексту разместится прямо на крае клиентской области.

Это приложение создано лишь с целью показать семейства шрифтов, поэтому код упрощенный и во многих случаях делает вещи не так, как должно делаться в настоящих приложениях. Например, здесь жестко закодировано предполагаемое значение размера документа вместо вычисленного, который мог бы определить, сколько пространства потребуется реально для вывода списка семейств шрифтов. (Мы будет использовать более правильный подход в следующем примере). Поэтому метод InitializeComponent() выглядит подобным образом:

private void InitializeComponent() {

this.components = new System.ComponentModel.Container();

this.Size = new System.Drawing.Size(300, 300);

this.Text = "EnumFontFamilies";

this.BackColor = Color.White;

this.AutoScrollMinSize = new Size(200, 500);

}

А вот метод OnPaint():

protected override void OnPaint(PaintEventArgs e) {

int verticalCoordinate = margin;

Point topLeftCorner;

InstalledFontCollection insFont = new InstalledFontCollection();

FontFamily [] families = insFont.Families;

e.Graphics.TranslateTransform(AutoScrollPosition.X, AutoScrollPosition.Y);

foreach (FontFamily family in families) {

if (family.IsStyleAvailable(FontStyle.Regular)) {

Font f = new Font(family.Name, 10);

topLeftCorner = new Point(margin, verticalCoordinate);

verticalCoordinate += f.Height;

e.Graphics.DrawString(family.Name, f, Brushes.Black, topLeftCorner);

f.Dispose();

}

}

base.OnPaint(e);

}

Этот код начинается с использования объекта InstalledFontCollection для получения массива, содержащего данные обо всех доступных семействах шрифтов. Для каждого семейства создается экземпляр шрифта размером 10 пунктов. Здесь для Font используется простой конструктор, существует множество других, которые требуют определения большего числа параметров. Выбранный конструктор использует два параметра: имя семейства и размер шрифта:

Font f = new Font(family.Name, 10);

Конструктор создает шрифт обычного стиля (т. е. не подчеркнутый, не курсив, не перечеркнутый). Однако на всякий случай сначала проверим, что этот стиль доступен для каждого семейства шрифтов, прежде чем пытаться вывести что-либо с его помощью. Это делается с применением метода FontFamily. IsStyleAvailable(), и такая проверка важна, так как не все шрифты доступны во всех стилях:

if (family.IsStyleAvailable(FontStyle.Regular))

FontFamily.IsStyleAvailable() получает один параметр — перечисление FontStyle. Это перечисление содержит ряд флажков, комбинирующихся с помощью оператора OR. Возможными флажками являются Bold, Italic, Regular, Strikeout и Underline.

Наконец, отметим, что здесь используется свойство Height класса Font, которое определяет высоту, необходимую для вывода текста этим шрифтом с учетом интервала между строками.

Font f = new Font (family.Name, 10);

topLeftCorner = new Point(margin, verticalCoordinate);

VerticalCoordinate += f.Height;

Для упрощения кода используемая версия OnPaint() демонстрирует несколько слабых приемов программирования. Вначале мы не подумали проверить, какую область документа в действительности надо нарисовать, мы просто пытаемся вывести все. Создание экземпляра Font, как отмечалось ранее, является интенсивным вычислительным процессом, поэтому на самом деле необходимо сохранять шрифты, а не создавать экземпляры новых копий всякий раз, когда вызывается OnPaint(). Мы заметили, что этот пример требует для своего рисования немало времени. Чтобы сберечь память и помочь сборщику мусора, мы вызываем Dispose() на каждом экземпляре шрифта после завершения с ним работы. Если этого не сделать, то после 10 или 20 операций рисования будет существовать большой объем бесполезно используемой памяти, хранящей шрифты, которые больше не требуются.

Редактирование текстового документа: пример CapsEditor

Мы переходим теперь к самому большому примеру этой главы. Пример CapsEditor создан для иллюстрации того, как принципы рисования, которые были до сих пор изучены, применить в более реальных условиях. Здесь не требуется никакого нового материала, кроме ответа на ввод пользователя с помощью мыши, но будет показано, как управлять рисованием текста так, чтобы приложение поддерживало производительность, гарантируя в то же время, что содержимое клиентской области основного окна всегда актуально.

Программа CapsEditor функционально является совсем простой. Она позволяет пользователю прочитать текстовый файл, который затем выводится построчно в клиентской области. Если пользователь делает двойной щелчок на любой строке, она будет изменяться в символы верхнего регистра. Это фактически все, что делает пример. Даже с таким ограниченным набором свойств мы обнаружим, что работа, вовлеченная в реализацию того, чтобы все выводилось в нужном месте, учитывая при этом вопросы производительности (выводить только то, что нужно, в данном вызове OnPaint()), будет достаточно сложно. В частности, мы имеем здесь новый элемент, связанный с изменением содержимого документа, происходящим в то время, когда пользователь выбирает пункт меню для считывания нового файла, либо когда он делает двойной щелчок, чтобы перевести строку в верхний регистр. В первом случае нам необходимо исправить размер документа, чтобы панели прокрутки по-прежнему работали правильно, и снова все вывести на экран. Во втором случае надо тщательно проверить, изменился ли размер документа и какой текст необходимо перерисовать.

Дадим обзор внешнего представления CapsEditor. Когда приложение начинает выполняться, оно не имеет загруженного документа и выводит:

Меню File имеет два пункта: Open и Exit. Exit заканчивает приложение, в то время как Open выводит стандартное диалоговое окно для открытия файла и считывает файл, который выбирает пользователь. На снимке показано использование CapsEditor для просмотра своего собственного файла исходного кода Form1.cs. Там также случайным образом были сделаны двойные щелчки мышью на нескольких строчках для преобразования их в верхний регистр:

Размеры вертикальной и горизонтальной панелей прокрутки являются, кстати, правильными. Клиентская область будет прокручиваться так, чтобы можно было просмотреть весь документ. CapsEditor не пытается переносить строки текста, пример уже усложнен в достаточной степени и без этого. Программа просто выводит каждую строку файла точно так, как ее читает. Не существует ограничений на размер файла, но предполагается, что это текстовый файл, который не содержит никаких непечатных символов.

Добавляем некоторые поля в класс Form1, которые нам понадобятся:

#region constant fields

private const string standardTitle = "CapsEditor";

// текст по умолчанию в заголовке

private const uint margin = 10;

// горизонтальное и вертикальное поля в клиентской области

#endregion

#region Member fields

private ArrayList documentLines = new ArrayList(); // "документ"

private uint lineHeight;   // высота одной строки в. пикселях

private Size documentSize; // какой требуется размер клиентской

// области для вывода документа

private uint nLines;       // число строк в документе

private Font mainFont;     // шрифт, используемый для вывода

// всех строк

private Font emptyDocumentFont; // шрифт, используемый для вывода

// сообщения Empty

private Brush mainBrush = Brushes.Blue;

// кисть, используемая для вывода текста документа

private Brush emptyDocumentBrush = Brushes.Red;

// кисть, используемая для вывода сообщения empty document

private Point mouseDoubleClickPosition;

// положение мыши при двойном щелчке

private OpenFileDialog fileOpenDialog = new OpenFileDialog();

// стандартный диалог открытия файла

private bool documentHasData = false;

// задать как true, если документ содержит данные

#endregion

Поле documentLines является ArrayList, который содержит прочитанный текст файла. В реальном смысле это поле содержит данные документа. Каждый элемент DocumentLines включает данные одной строки текста, который был считан. Этот объект ArrayList предпочтительнее обычного массива C#, так что можно динамически добавлять в него элементы, когда считывается файл. Можно заметить, что достаточно свободно используются директивы препроцессора #region для объединения в блоки частей программы, чтобы ее было легче редактировать.

Как было сказано, каждый элемент documentLines содержит информацию о строке текста. Эта информация является на самом деле экземпляром другого класса, который был определен — TextLineInformation:

class TextLineInformation {

public string Text;

public uint Width;

}

TextLineInformation выглядит как классический случай обычного использования структуры, а не класса, так как требуется только сгруппировать поля. Однако его экземпляры всегда доступны в качестве элементов класса ArrayList, в котором они хранятся как ссылочные типы. Поэтому объявление TextLineInformation классом приводит к более эффективным действиям, сохраняя множество операций упаковки и распаковки.

Каждый экземпляр TextLineInformation хранит строку текста и выводится как один элемент. Обычно для каждого такого элемента в приложении GDI+ желательно сохранять его текст, а также мировые координаты, где он должен выводиться, и размер. Обратите внимание, что используются мировые координаты, а не координаты страницы. Координаты страницы часто изменяются, когда пользователь прокручивает текст, в то время как мировые координаты меняются лишь в случае, когда другие части документа преобразуются каким-то образом. В данном случае мы сохранили только Width элемента, так как высота здесь является просто высотой выбранного шрифта. Она одинакова для всех строк текста, поэтому нет смысла хранить ее отдельно для каждой строки. Вместо этого она сохраняется только однажды в поле Form1.lineHeight. Что касается позиции, то в данном случае координата х просто равна граничному полю, а координата у легко вычисляется как:

Margin + LineHeight*(количество строк выше текущей строки)

Если бы мы попытались выводить и манипулировать, скажем, отдельными словами вместо полных строк, то позиция х каждого слова должна была бы вычисляться с помощью значений ширины всех предыдущих слов в этой строке текста. Но чтобы сохранить пример простым, мы интерпретируем каждую строку текста как один элемент.

Займемся теперь основным меню. Эта часть приложения принадлежит к формам Windows — тема рассмотрения главы 9. Пункты меню были добавлены с помощью графического представления в Visual Studio.NET, но переименованы как menuFileOpen и menuFileExit. Затем код в InitializeComponent() был изменен, чтобы добавить подходящие обработчики событий, а также выполнить некоторую инициализацию:

private void InitializeComponent() {

// добавлено мастером построения кода

this.menuFileOpen = new System.Windows.Forms.MenuItem();

this.menuFileExit = new System.Windows.Forms.MenuItem();

this.mainMenu1 = new System.Windows.Forms.MainMenu();

this.menuFile = new System.Windows.Forms.MenuItem();

this.menuFileOpen.Index = 0;

this.menuFileOpen.Text = "Open";

this.menuFileExit.Index = 3;

this.menuFileExit.Text = "Exit";

this.mainMenu1.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {this.menuFile});

this.menuFile.Index = 0;

this.menuFile.MenuItems.AddRange(

new System.Windows.Forms.MenuItem[] {this.menuFileOpen, this.menuFileExit});

this.menuFile.Text = "File";

this.menuFileOpen.Click +=

new System.EventHandler(this, menuFileOpen_Click);

this.menuFileExit.Click +=

new System.EventHandler(this.menuFileExit_Click);

this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);

this.BackColor = System.Drawing.Color.White;

this.Size = new Size(600, 400);

this.Menu = this.mainMenu1;

this.Text = standardTitle;

CreateFonts();

FileOpenDialog.FileOk +=

new System.ComponentModel.CancelEventHandler(this.OpenFileDialog_FileOk);

}

Мы добавили обработчики событий для пунктов меню File и Exit, а также для диалога FileOpen, который выводится, когда пользователь выбирает Open. CreateFonts() является вспомогательным методом, выбирающим шрифты:

private void CreateFonts() {

mainFont = new Font("Arial", 10);

lineHeight = (uint)mainFont.Height;

emptyDocumentFont = new Font("Verdana", 13, FontStyle.Bold);

}

Реальное определение обработчиков является достаточно стандартным материалом:

protected void OpenFileDialog_FileOk(object Sender, CancelEventArgs e) {

this.LoadFile(fileOpenDialog.FileName);

}

protected void menuFileOpen_Click(object sender, EventArgs e) {

fileOpenDialog.ShowDialog();

}

protected void menuFileExit_Click(object sender, EventArgs e) {

this.Close();

}

Исследуем метод LoadFile(). Он занимается открытием и считыванием файла (а также обеспечивает инициирование события Paint, чтобы была выполнена перерисовка с новым файлом).

private void LoadFile(string FileName) {

StreamReader sr = new StreamReader(FileName);

string nextLine;

documentLines.Clear();

nLines = 0;

TextLineInformation nextLineInfo;

while ((nextLine = sr.ReadLine()) != null) {

nextLineInfo = new TextLineInformation();

nextLineInfо.Text = nextLine;

documentLines.Add(nextLineInfo); ++nLines;

}

sr.Close();

documentHasData = (nLines > 0) ? true : false;

CalculateLineWidths();

CalculateDocumentSize();

this.Text = standardTitle + " " + FileName;

this.Invalidate();

}

Большая часть этой функции является просто стандартным кодом чтения файла (см. главу 14). Обратите внимание, что по мере чтения файла мы последовательно добавляем строки в documentLines ArrayList, поэтому массив в конце содержит информацию для каждой строки по порядку. После считывания файла устанавливается флаг documentHasData для указания, что действительно что-то есть для вывода на экран. Далее определяется содержание и место, а затем клиентская область, т.е. размер документа влияющий на задание панелей прокрутки. В конце добавляется текст строки заголовка и вызывается метод Invalidate(). Invalidate() является важным методом, поставляемым Microsoft. Поэтому мы постараемся объяснить его использование, прежде чем переходить к коду методов CalculateLineWidths() и CalculateDocumentSize().

Метод Invalidate()

Invalidate() является членом System.Windows.Forms.Form, с которым мы еще не встречались. Это очень полезный метод для случая, когда что-то необходимо перерисовать. По сути он отмечает область клиентского окна как недействительную и поэтому требующую перерисовки, а затем обеспечивает инициирование события Event. Существует две перезагружаемые версии метода Invalidate(): можно передать ему прямоугольник, который точно определяет (в координатах страницы), какая область окна требует перерисовки, или, если не передается никаких параметров, вся клиентская область помечается как недействительная.

Почему мы делаем это таким образом? Если мы знаем, что требуется что-то нарисовать, почему не вызвать просто OnPaint() или другой метод? При необходимости сделать на экране очень точное небольшое изменение, можно так и поступить, но обычно вызов процедур рисования напрямую рассматривается как плохая практика программирования. Если код решает, что требуется выполнить некоторое рисование, обычно вызывается метод Invalidate().

Для этого существует несколько причин:

□ Рисование почти всегда является наиболее интенсивно использующей процессор задачей, которую будет решать приложение GDI+. Выполнение его в середине другой работы задерживают эту работу. Для нашего примера, если бы метод рисования вызывался прямо из метода LoadFile(), то метод LoadFile() не вернул бы управление, пока не завершена задача рисования. В течение этого времени приложение не сможет ответить ни на одно иное событие. С другой стороны, вызывая метод Invalidate(), мы просто поручаем Windows инициировать событие Paint перед непосредственным выходом из LoadFile(). Система Windows тогда сможет проверить события, ожидающие обработки. Внутренне это происходит следующим образом. События находятся в так называемых сообщениях, которые выстраиваются в очередь. Система Windows периодически проверяет очередь сообщений, и если в ней есть события, Windows берет одно из них и вызывает соответствующий обработчик событий. С большой вероятностью событие Paint — единственное событие, находящееся в очереди, поэтому OnPaint() будет немедленно вызван в любом случае. Однако в более сложных приложениях могут быть другие события, некоторые из них имеют приоритет. В частности, если пользователь решил покинуть приложение, это будет отмечено сообщением в очереди, известном как WM_QUIT. Обработка такого события будет иметь самый высокий приоритет. Это очевидно, так как выполнение, например обновления графики в окне приложения, которое в данный момент будет закрыто, не имеет смысла. Таким образом, использование для рисования области метода Invalidate() сортировки запросов означает, что приложение действует, как хорошо ведущее себя приложение Windows.

□ В случае более сложного, мультипоточного приложения желательно, чтобы только один поток выполнения обрабатывал все рисование. Использование метода Invalidate() для направления всего рисования в очередь сообщений предоставляет гарантию, что один и тот же поток выполнения (какой бы поток выполнения ни отвечал за очередь сообщений, это будет поток выполнения, который вызвал Application.Run()) произведет рисование безотносительно к тому, какие другие потоки выполнения запрашивают операцию рисования.

□ Существует также причина, связанная с производительностью. Предположим, что примерно в одно время поступает пара различных запросов для рисования части экрана. Возможно, что код только что сделал изменение документа и хочет убедиться, что обновленный документ выводится, а в то же самое время пользователь восстановил минимизированное окно или переместил другое окно, которое закрывало часть клиентской области. Вызывая Invalidate(), мы даем знать окну, что это произошло. Windows может затем объединить события Paint, если это возможно, комбинируя недействительные области, так что рисование будет выполняться только один раз. (Вызов метода для выполнения рисования прямо из кода может привести к ненужному результату, когда одна и та же область экрана будет перерисовываться более одного раза.)

□ Наконец, код для выполнения рисования будет, вероятно, одной из наиболее сложных частей кода приложения, особенно если у приложения достаточно развитый интерфейс пользователя. Те, кому придется модифицировать этот код через пару лет, будут благодарить разработчика за сохранение кода рисования в одном месте и за максимально возможную простоту — чего добиться гораздо легче, если код не разбросан по разными частям программы.

Из этого можно сделать вывод, что хорошая практика состоит в поддержании всего кода рисования в процедуре OnPaint() или в других методах, вызываемых из этого метода. Не создавайте в коде множества других мест, которые вызывают методы для реализации странных фрагментов рисования, все-таки аспекты создания программы должны быть сбалансированы относительно различных рассмотрений. Если, предположим, требуется заменить только один символ или фигуру на экране или добавить акцент к букве и совершенно точно известно, что это не повлияет ни на какие другие изображения, то можно не пользоваться методом Invalidate(), а написать просто отдельную процедуру рисования.

В очень сложном приложении можно даже написать целый класс, который отвечает за рисование на экране. Несколько лет назад, когда MFC были стандартной технологией для приложений с интенсивным использованием GDI, MFC следовали этой модели с помощью класса C++ с именем С<Имя_приложения>View, который отвечал за это. Однако даже в таком случае этот класс имел функцию-член OnDraw(), которая была создана, чтобы быть точкой входа для большинства запросов рисования.

Вычисление размеров объектов и размера документа

Мы возвращаемся теперь к примеру CapsEditor и разбираем методы CalculateLineWidths() и CalculateDocumentSize(), которые вызываются из метода LoadFile():

private void CalculateLineWidths() {

Graphics dc = this.CreateGraphics();

foreach (TextLineInformation nextLine in documentLines) {

nextLine.Width = (uint)dc.MeasureString(nextLine.Text, mainFont).Width;

}

}

Этот метод просто выполняется на каждой прочитанной строке и использует метод Graphics.MeasureString() для определения и сохранения значения величины горизонтального пространства экрана, которое требуется строке. Мы сохраняем значение, так как MeasureString() является весьма интенсивным с вычислительной точки зрения. Так как в нашем примере CapsEditor не слишком легко определить высоту и положение каждого элемента, то этот метод почти наверняка будет реализован, чтобы вычислить все эти величины.

Теперь, когда мы знаем размер каждого элемента на экране и можем вычислить приблизительное его положение, определим реальный размер документа. Высота, по существу, равна числу строк, умноженному на высоту каждой строки. Ширину необходимо определить просмотром всех строк, чтобы выявить самую длинную и взять ширину этой строки. Для высоты и ширины также желательно допустить небольшие поля вокруг выводимого документа, чтобы приложение выглядело более привлекательно. (Нежелательно, чтобы текст прикасался к одному из углов клиентской области.) Вот метод, который вычисляет размер документа:

private void CalculateDocumentSize() {

if (!documentHasData) {

documentSize = new Size(100, 200);

} else {

documentSize.Height = (int)(nLines*lineHeight) + 2*(int)margin;

uint maxLineLength = 0;

foreach (TextLineInformation nextWord in documentLines) {

uint tempLineLength = nextWord.Width + 2*margin;

if (tempLineLength > maxLineLength) maxLineLength = tempLineLength;

}

documentSize.Width = (int)maxLineLength;

}

this.AutoScrollMinSize = documentSize;

}

Этот метод сначала проверяет, есть ли данные для вывода. Если данных нет, мы слегка схитрим и зададим жестко кодированный размер документа такой величины, чтобы хватило места для выведения большими красными буквами предупреждения <Empty Document>. В противном случае необходимо воспользоваться методом MeasureString() для определения реального размера документа.

После этого размер документа сообщается экземпляру класса Form, задавая свойство Form.AutoScrollMinSize. Когда это сделано, за сценой происходит кое-что интересное. В процессе задания этого свойства клиентская область становится недействительной и инициируется событие Paint в связи с тем, что изменение размера документа означает необходимость добавить или изменить панели прокрутки, а также, что вся клиентская область почти наверняка будет перерисована. Это в полной мере иллюстрирует то, что было сказано ранее об использовании метода Form.Invalidate(). Если вернуться назад к коду LoadFile(), то станет понятно, что вызов метода Invalidate() в этом методе является на самом деле излишним. Клиентская область будет объявлена недействительной в любом случае, когда задается размер документа. Явный вызов метода Invalidate() в реализации метода LoadFile() оставлен для иллюстрации. Фактически в этом случае все, что будет делать вызванный метод Invalidate(), является ненужным запросом повторного события Paint. Однако это в свою очередь подтверждает, что Invalidate() дает Windows возможность оптимизировать производительность. Второе событие Paint не будет фактически инициировано: Windows увидит, что в очереди уже находится событие Paint, и сравнит запрошенные недействительные области, чтобы попробовать объединить их. В этом случае оба события Paint будут определять всю клиентскую область, поэтому ничего не нужно делать, и Windows спокойно удалит второй запрос Paint. Конечно, это действие займет какое-то процессорное время, но оно будет ничтожным по сравнению с тем, сколько времени потребуется для реального выполнения рисования.

OnPaint()

Итак, мы увидели, как CapsEditor загружает файл. Теперь пришло время посмотреть, как выполняется рисование:

protected override void OnPaint(PaintEventArgs e) {

Graphics dc = e.Graphics;

int scrollPositionX = this.AutoScrollPosition.X;

int scrollPositionY = this.AutoScrollPosition.Y;

dc.TranslateTransform(scrollPositionX, scrollPositionY);

if (!documentHasData) {

dc.DrawString("<Empty Document>", emptyDocumentFont,

emptyDocumentBrush, new Point(20, 20));

base.OnPaint(e);

return;

}

// определить, какие строки находятся в вырезанном прямоугольнике

int minLineInClipRegion =

WorldYCoordinateToLineIndex(е.ClipRectangle.Top - scrollPositionY);

if (minLineInClipRegion == -1) minLineInClipRegion = 0;

int maxLineInClipRegion =

WorldYCoordinateToLineIndex(e.ClipRectangle.Bottom - scrollPositionY);

if (maxLineInClipRegion >= this.documentLines.Count || maxLineInClipRegion == -1)

maxLineInClipRegion = this.documentLines.Count - 1;

TextLineInformation nextLine;

for (int i = minLineInClipRegion; i <= maxLineInClipRegion; i++) {

nextLine = (TextLineInformation)documentLines[i];

dc.DrawString(nextLine.Text, mainFont, mainBrush, this.LineIndexToWorldCoordinates(i));

}

base.OnPaint(e);

}

В середине этой перезагружаемой версии OnPaint() находится цикл, который перебирает все строки документа, вызывая метод Graphics.DrawString() для рисования каждой из них. Остальная часть этого кода связана в основном с оптимизацией рисования — обычный материал для определения, что действительно необходимо нарисовать вместо необдуманного приказания экземпляру Graphics перерисовать все.

Мы начинаем с проверки, имеются ли в документе какие-либо данные. Если данных нет, мы выводим краткое сообщение, говорящее об этом вызываем реализацию OnPaint() из базового класса и выходим. Если имеются данные, то мы начинаем проверять прямоугольник вырезания. Способ, которым это делается, состоит в вызове другого написанного нами метода WorldYCoordinateToLineIndex(). Мы рассмотрим этот метод позже, но по сути он получает заданную у-позицию относительно верха документа и определяет, какая строка документа будет выводиться в этой точке.

При первом вызове метода WorldYCoordinateToLineIndex() ему передается значение координаты е.ClipRectangle.Top - scrollPositionY. Это верх области вырезания, преобразованный в мировые координаты. Если возвращаемое значение будет -1, мы предположим, что нам нужно начать с начала документа (если верх области вырезания находится наверху граничного поля).

После того, как все это будет сделано, мы практически повторяем тот же процесс для низа прямоугольника вырезания, чтобы определить последнюю строку документа, которая находится внутри области вырезания. Индексы первой и последней строки хранятся соответственно в minLineInClipRegion и maxLineInClipRegion, поэтому мы можем просто выполнить цикл for между этими значениями, чтобы реализовать рисование. Внутри цикла рисования мы должны сделать приблизительно обратное преобразование для преобразования, выполненного методом WorldYCoordinateToLineIndex(). Задан индекс строки текста и нужно проверить, где она должна быть нарисована. Это вычисление является вполне простым, но мы поместили его в другой метод LineIndexToWorldCoordinates(), который возвращает требуемые координаты верхнего левого угла элемента. Возвращаемые координаты являются мировыми координатами, и это хорошо, так как мы уже вызвали метод TranslateTransform() на объекте Graphics, поэтому нам нужно передать ему при запросе вывода элемента мировые координаты, а не координаты страницы.

Преобразования координат

В этом разделе мы рассматриваем реализацию вспомогательных методов, которые были использованы в примере CapsEditor, чтобы выполнить преобразование координат. Это методы WorldYCoordinateToLineIndex() и LineIndexToWorldCoordinates(), на которые мы ссылались в предыдущем разделе, а также некоторые другие методы.

Первое. LineIndexToWorldCoordinates() получает заданный индекс строки и определяет мировые координаты верхнего левого угла строки с помощью известных ширины поля и высоты строки:

private Point LineIndexToWorldCoordinates(int index) {

Point TopLeftCorner =

new Point((int)margin, (int)(lineHeight*index + margin));

return TopLeftCorner;

}

Мы также используем метод, который делает приблизительно обратное преобразование в OnPaint(). WorldYCoordinateToLineIndex() определяет индекс строки, но он принимает в расчет только вертикальную мировую координату. Это связано с тем, что метод используется для определения индекса строки, соответствующего верху и низу области вырезания:

private int WorldYCoordinateToLineIndex(int у) {

if (у < margin) return -1;

return (int)((y - margin)/lineHeight);

}

Существуют еще три метода, вызываемые из процедуры обработки, которые отвечают на двойной щелчок пользователя мышью. Прежде всего — метод, определяющий индекс строки, которая выведется в заданных мировых координатах. В отличие от WorldYCoordinateToLineIndex() этот метод берет в расчет позиции x- и y- координат. Он возвращает -1, если нет строки текста с заданными координатами.

private int WorldCocrdinatesToLineIndex(Point position) {

if (!documentHasData) return -1;

if (position.Y < margin || position.X < margin) return -1;

int index = (int) (position.Y - margin) / (int) this.lineHeight;

// проверить, что позиция находится не ниже документа

if (index >= documentLines.Count) return -1;

// теперь проверим, что горизонтальная позиция располагается

// внутри строки

TextLineInformation theLine =

(TextLineInformation)documentLines[index];

if (position.X > margin * theLine.Width)

return -1;

// все хорошо. Можно вернуть ответ.

return index;

}

Наконец, иногда необходимо делать преобразование между индексом строки и координатами страницы, а не мировыми координатами. Это делает следующий метод:

private Point LineIndexToPageCoordinates(int index) {

return LineIndexToWorldCoordinates(index) + new Size(AutoScrollPosition);

}

private int PageCoordinatesToLineIndex(Point position) {

return WorldCoordinatesToLineIndex(position - new Size(AutoScrollPosition));

}

Эти методы сами по себе не кажутся особенно интересными, они иллюстрируют общую технику, которую, по всей видимости, вам придется часто использовать. Применяя GDI+, мы иногда оказываемся в ситуации где заданы некоторые координаты (например, координаты места, где пользователь щелкнул мышью) и требуется определить, какой элемент изображен в этом месте. Или наоборот, для заданного определенного элемента вывода необходимо приблизительно определить, где он должен быть выведен. Следовательно, при создании приложений GDI+ может оказаться полезным умение написать методы, эквивалентные методам преобразований координат, проиллюстрированным здесь.

Ответ на ввод пользователя

До сих пор, за исключением пеню File в примере CapsEditor, все, что делались в этой главе, было односторонним, так как приложение общалось с пользователем, выводя информацию на экране. Почти любое программное обеспечение работает в обоих направлениях — пользователь также может общаться с программным обеспечением. Мы собираемся теперь добавить эту возможность в CapsEditor.

Заставить приложение GDI+ ответить на пользовательский ввод в действительности значительно проще, чем писать код для рисования на экране, и мы в главе 9 уже рассмотрели, как обрабатывать ввод пользователя. По сути для этого необходимо переопределить методы из класса Form, которые вызываются из соответствующего обработчика событий почти так же, как вызывается OnPaint(), когда инициируется событие Paint.

Для случая обнаружения, когда пользователь щелкнул мышью или переместил мышь, функции которые можно будет переопределить включают следующие:

Метод

Когда вызывается

OnClick(EventArgs е)

Сделан щелчок мышью

OnDoublеСlick(EventArgs е)

Сделан двойной щелчок мышью

OnMouseDown(MouseEventArgs е)

Нажата левая кнопка мыши

OnMouseHover(MouseEventArgs е)

Мышь остается неподвижной после перемещения

OnMouseMove(MouseEventArgs е)

Мышь перемещается

OnMouseUp(MouseEventArgs e)

Левая кнопка мыши отпущена

Если требуется определить, когда пользователь вводит с клавиатуры какой-то текст, то, вероятно, можно будет переопределить следующие методы.

OnKeyDown(KeyEventArgs е)

Клавиша нажата

OnKeyPress(KeyPressEventArgs е)

Клавиша нажата и отпущена

OnKeyUp(KeyEventArgs e)

Нажатая клавиша отпущена

Отметим, что некоторые из этих событий перекрываются. Например, нажатие пользователем кнопки мыши порождает событие MouseDown. Если кнопка немедленно снова освобождается, то это порождает событие MouseUp и событие Click. Также некоторые из этих методов получают аргумент, который выводится из аргумента EventArgs и поэтому может использоваться для предоставления дополнительных данных об определенном событии. MouseEventArgs имеет два свойства — X и Y, которые задают координаты мыши во время нажатия кнопки. KeyEventArgs и KeyPressEventArgs имеют свойства, указывающие, какая клавиша или клавиши имеют отношение к событию.

Затем разработчик должен продумать логику последующих действий. Только одно замечание. Вполне вероятно, что приложение GDI+ потребует создания большего объема логики, чем приложение Windows.Forms. Это связано с тем, что в приложении Windows.Forms приходится отвечать на высокоуровневые события (например, TextChanged для текстового поля). При использовании GDI+ события являются более базовыми — пользователь щелкает мышью или нажимает клавишу h. Действие, которое предпринимает приложение, скорее всего зависит от последовательности событий, а не от одного события. Например, в Word for Windows, чтобы выбрать некоторый текст, пользователь обычно щелкает левой кнопкой мыши, перемещает мышь и освобождает левую кнопку мыши. Если пользователь просто нажмет, а затем освободит левую кнопку мыши, Word не выделит никакой текст, он просто переместит курсор текста в место, где был курсор мыши. Поэтому в точке, где пользователь нажимает левую кнопку мыши, нельзя еще сказать, что пользователь собирается делать. Приложение будет получать событие MouseDown, но, предположим, что требуется, чтобы приложение вело себя так же, как это делает Word for Windows. Нам ничего не остается, кроме как записать, что произошел щелчок мыши с курсором в определенной позиции. Когда будет получено событие MouseMove, вы захотите проверить в только что сделанных записях, не нажата ли в данный момент левая кнопка, и если это так, то выделить текст, поскольку пользователь его выбрал. Когда пользователь освобождает левую кнопку мыши (в методе OnMouseUp()), происходит проверка, было ли выполнено какое-либо перемещение, пока мышь была нажата, а далее нужно действовать соответственно. Только в этой точке последовательность завершается.

Другой момент нашего рассмотрения связан с перекрытием некоторых событий, так как в этой ситуации возникает необходимость выбора события, на которое должен ответить код.

Золотое правило состоит в тщательном продумывании логики каждой комбинации движения мыши или щелчка и события клавиатуры, которое может инициировать пользователь, а также обеспечении того, чтобы приложение отвечало способом, который интуитивно понятен и согласован с обычно ожидаемым поведением приложений в каждом случае. Большая часть работы здесь будет состоять в обдумывании, а не в кодировании, хотя кодирование потребуется достаточно кропотливое, поэтому может понадобиться принять во внимание множество комбинаций ввода пользователя. Например, что должно делать приложение, если пользователь начинает вводить текст, когда одна из кнопок мыши нажата? Это покажется невероятной комбинацией, но рано или поздно какой-то пользователь попробует так сделать.

Для примера CapsEditor все сделано очень простым, по сути нет никаких комбинаций для рассмотрения. Необходимо ответить только на двойной щелчок пользователя мышью, и в этом случае строку текста, над которой находится курсор мыши, необходимо перевести в символы верхнего регистра.

Это несложная задача, но имеется одна заминка. Необходимо перехватить событие Doubleclick, но приведенная выше таблица показывает, что это событие имеет параметр EventArgs, а не параметр MouseEventArgs. Проблема в том, что необходимо знать, где находится мышь, когда пользователь делает двойной щелчок, если требуется правильно определить строку текста, которая будет переводиться в верхний регистр, и для этого требуется параметр MouseEventArgs. Существует два способа обойти эту проблему. Один состоит в использовании статического метода Control.MousePosition, который реализован объектом Form1, чтобы найти положение мыши, как в следующем коде:

protected override void OnDoubleClick(EventArgs e) {

Point MouseLocation = Control.MousePosition;

// обработать двойной щелчок

В большинстве случаев это срабатывает. Однако могут быть сложности, если приложение (или даже некоторое другое приложение с более высоким приоритетом) выполняет некоторые интенсивные вычисления в момент двойного щелчка пользователя. В таком случае возможно, что обработчик событий OnDoubleClick() не будет вызван в течение полсекунды. Задержки такого рода вряд ли желательны, так как они начинают раздражать пользователей достаточно быстро. Половины секунды вполне достаточно для перемещения мыши на половину экрана, — и в этом случае выполнение OnDoubleClick() может произойти в совершенно другом месте.

Лучший подход состоит в использовании одного из многих наложений между значениями событий мыши. Первая часть двойного щелчка мыши включает нажатие левой кнопки. Это означает, что если вызывается OnDoubleClick(), то мы знаем, что также был вызван OnMouseDown() с курсором мыши в том же месте. Можно использовать перезагружаемую версию OnMouseDown() для записи положения мыши при использовании в OnDoubleClick(). Этот подход используется в CapsEditor:

protected override void OnMouseDown(MouseEventArgs e) {

base.OnMouseDown(e);

this.mouseDoubleClickPosition = new Point(e.X, e.Y);

}

Теперь посмотрим на перезагруженную версию OnDoubleClick(). Здесь придется выполнить немного больше работы:

protected override void OnDoubleClick(EventArgs e) {

int i = PageCoordinatesToLineIndex(this.mouseDoubleClickPosition);

if (i >= 0) {

TextLineInformation lineToBeChanged =

(TextLineInformation) documentLines[i];

lineToBeChanged.Text = lineToBeChanged.Text.ToUpper();

Graphics dc = this.CreateGraphics();

uint newWidth = (uint)dc.MeasureString(lineToBeChanged.Text, mainFont).Width:

if (newWidth > lineToBeChanged.Width) lineToBeChanged.Width = newWidth;

if(newWidth+2*margin > this.documentSize.Width) {

this.documentSize.Width = (int)newWidth;

this.AutoScrollMinSize = this.documentSize;

}

Rectangle changedRectangle =

new Rectangle(

LineIndexToPageCoordinates(i), new Size((int)newWidth, (int)this.lineHeight));

this.Invalidate(changedRectangle);

}

base.OnDoubleClick(e);

}

Начнем работу с вызова PageCoordinatesToLineIndex() для определения, над какой строкой текста находится курсор мыши, когда пользователь делает двойной щелчок. Если этот вызов возвращает -1, то никакого текста под курсом нет, поэтому ничего делать не надо (за исключением, конечно, вызова версии OnDoubleClick() базового класса, чтобы позволить Windows выполнить обработку по умолчанию. Это никогда не надо забывать делать.).

При условии, что была идентифицирована строка текста, можно воспользоваться методом string.ToUpper(), чтобы легко преобразовать ее в верхний регистр. Труднее определить, что и где необходимо перерисовать. К счастью, так как пример сделан упрощенным, существует не слишком много комбинаций. Можно предположить для начала, что преобразование в верхний регистр будет всегда либо оставляет ширину строки на экране без изменения, либо увеличивает ее. Заглавные буквы больше строчных, поэтому ширина никогда не уменьшится. Известно, что поскольку мы не переносим строки, строка текста не будет продолжена на следующей строке и не сместит текст ниже. Действие по преобразованию строки в верхний регистр не будет поэтому в действительности изменять положение ни одного из выводимых элементов. Это существенное упрощение.

Затем код использует Graphics.MeasureString() для определения новой ширины текста. Здесь имеется две возможности:

□ Первая: новая ширина может сделать строку самой длинной строкой, что приведет к увеличению ширины всего документа. В этом случае необходимо задать для AutoScrollMinSize новый размер, чтобы панели прокрутки размещались правильно.

□ Вторая: размер документа может не измениться.

В любом случае необходимо перерисовать изображение на экране, вызывая Invalidate(). Только одна строка изменилась, поэтому нет необходимости перерисовывать весь документ. Вместо этого надо определить границы прямоугольника, который содержит только измененную строку, чтобы можно было передать этот прямоугольник в метод Invalidate() для перерисовывания только этой строки текста. Именно так делает представленный выше код. Вызов Invalidate() приведет к вызову OnPaint(), когда окончательно закончит работу обработчик события мыши. Вспомните предыдущие замечания в этой главе о трудностях в задании точки прерывания в OnPaint(). Если при выполнении примера задать точку прерывания в OnPaint() для перехвата получающегося действия рисования, то обнаружится, что параметр PaintEventArgs для OnPaint действительно содержит область вырезания, которая соответствует указанному прямоугольнику. И так как метод OnPaint() был перезагружен, чтобы аккуратно вычленить область вырезания, то будет перерисована только одна требуемая строка текста.

Печать

В этой главе мы полностью сконцентрировались на рисовании на экране. Часто бывает желательно, чтобы приложение могло создать твердую копию данных. К сожалению, в книге не хватает места, чтобы рассмотреть детали этого процесса, но мы кратко рассмотрим вопросы, которые встретятся при реализации печати документа.

Во многом печать схожа с выводом на экран. Предоставляется контекст устройства (экземпляр Graphics) и на этом экземпляре вызываются все обычные команды вывода. Однако имеются некоторые различия: принтеры не могут прокручиваться — они используют страницы. Необходимо убедиться, что найден разумный способ деления документа на страницы, и выводить каждую страницу по запросу. К тому же большинство пользователей ожидают, что вывод на принтер будет выглядеть очень похоже на вывод на экран. Этого очень трудно добиться при использовании координат страницы. Проблема в том, что принтеры имеют другое число точек на дюйм (dpi), чем экран. Дисплейные устройства традиционно поддерживают стандарт около 96 dpi, хотя некоторые новые мониторы имеют более высокое разрешение. Принтеры могут иметь более тысячи dpi. Это означает, например, что при рисовании фигур или выводе изображений, при задании их размеров числом пикселей они будет выглядеть на принтере слишком маленькими. Иногда та же самая проблема может влиять на шрифты текста. К счастью, GDI+ допускает в этих случаях применение координат устройства. Чтобы напечатать документы, почти наверняка придется использовать свойство Grpahics.PageUnit для выполнения печати с помощью некоторых физических единиц измерения, таких как дюймы или миллиметры.

.NET имеет большое количество классов, созданных для поддержки процесса печати. Эти классы позволяют контролировать и извлекать различные настройки принтера и находятся в основном в пространстве имен System.Drawing.Printing. Существуют также предопределенные диалоговые окна PrintDialog и PrintPreviewDialog, которые доступны в пространстве имен System.Windows.Forms. Процесс печати будет включать вызов метода Show() на экземпляре одного из этих классов после задания некоторых свойств.

Заключение

В этой главе было рассмотрено рисование на устройстве вывода, реализующиеся в коде приложения, а не с помощью предопределенных элементов управления или диалоговых окон. GDI+ является мощным инструментом, и базовые классы .NET могут помочь при рисовании на устройстве. Мы видели, что этот процесс является в действительности довольно простым, в большинстве случаев можно рисовать текст и сложные фигуры или выводить изображения с помощью пары инструкций C#. Однако управление рисованием — работа, происходящая неявно, включающая определение, что и где нарисовать и нужна ли перерисовка в любой данной ситуации. Это значительно более сложная задача, требующая тщательного проектирования алгоритма. Важно хорошо понимать, как работает GDI+ и какие действия предпринимает Windows для рисования. В частности, с учетом архитектуры Windows важно, чтобы рисование выполнялось с помощью объявления областей окна недействительными, где это возможно, в таком случае система Windows реагирует должным образом на событие Paint.

Существует значительное количество классов .NET, связанных с рисованием, которые невозможно рассмотреть в одной главе, но, зная основные принципы, вовлеченные в рисование, можно изучить их самостоятельно. Просматривая списки их методов в документации и создавая их экземпляры, вы поймете, что они делают. В конце концов, рисование, как почти и любой другой аспект программирования, требует логики, тщательного обдумывания и четких алгоритмов. Используйте это, и вы сможете написать сложные интерфейсы пользователя, не зависящие от стандартных элементов управления. Ваша программа существенно выиграет как в удобстве для пользователя, так и в визуальном представлении. Многие приложения целиком полагаются на элементы управления в своем интерфейсе пользователя. Хотя это и может быть эффективно, такие приложения очень быстро начинают походить друг на друга. Добавляя некоторый код GDI+, чтобы выполнить специальное рисование, можно выделить свое приложение и сделать его внешне более оригинальным.

Глава 22

Доступ в Интернет

В главах 16–18 было показано, как использовать C# для создания мощных и эффективных динамических страниц Web с помощью ASP.NET, а также служб Web. Клиенты, обращающиеся к страницам ASP.NET, по большей части будут пользователями Internet Explorer или какого-либо другого браузера Web. Но иногда необходимо, чтобы создаваемые приложения действовали как клиенты Web. Это может быть, например, если нужно добавить в создаваемые приложения свойства просмотра Web, или, если необходимо, чтобы создаваемые приложения программным путем получали информацию с некоторых web-сайтов. В последнем случае обычно лучше, чтобы сайт реализовал службу Web, но при обращении к внешним сайтам возможно отсутствие контроля за тем, как реализован сайт, и поэтому может не быть другой возможности выбора, кроме программного доступа к сайту, который реализован как стандартные страницы HTML, ASP или ASP.NET.

Именно эту сторону картины мы кратко рассмотрим сейчас. В частности, будут представлены средства базовых классов .NET для использования различных сетевых протоколов, например HTTP для доступа к сетям и Интернету в качестве клиента. Мы обсудим следующие вопросы:

□ Запрос данных из Web и получение ответа от серверов

□ Отправку данных HTTP POST

□ Извлечение информации заголовка HTTP из ответов серверов

Коротко коснемся средств, существующих для прямого доступа к службам нижнего уровня, таким как отправка и получение пакетов TCP и ожидание приема на определенных портах.

Пространства имен System.Net и System.Net.Sockets содержат большинство базовых классов .NET, которые связаны с работой в сети с точки зрения клиента. Пространство имен System.Net обычно связано с операциями более высокого уровня, например загрузкой и выгрузкой файлов и выполнением запросов Web с помощью HTTP и других протоколов, в то время как System.Net.Sockets содержит классы, связанные с операциями более низкого уровня. Они более полезны при работе непосредственно с сокетами или такими протоколами, как TCP/IP, и используются по большей части для сокрытия соответствующей функции API Windows.

В этой главе используется достаточно практичный подход: представлен ряд постепенно усложняющихся примеров в сопровождении соответствующей теории и с обсуждением сетевых концепций.

Мы начнем с простейшего случая, когда необходимо просто послать серверу запрос и сохранить или обработать полученную информацию.

Класс WebClient

Если необходимо выполнить запрос файла по определенному URL, то, скорее всего, самым простым классом .NET для использования в этом случае окажется System.Net.WebClient. Это класс максимально высокого уровня, созданный для выполнения базовых операций с помощью одной или двух команд.

Загрузка файлов

Существует два способа загрузки файлов с web-сайта с помощью WebClient, в зависимости от того, хотим ли мы файл сохранить или обработать его содержимое файла непосредственно внутри приложения. Если нужно просто сохранить файл, то вызывается метод DownloadFile(). Этот метод получает два параметра: URL, из которого необходимо извлечь файл, и имя файла (или путь доступа), в котором мы хотим сохранить файл.

WebClient Client = new WebClient();

Client.DownloadFile("http://www.Wrox.com/default.htm", "index.htm");

Более часто приложению приходится обрабатывать данные, полученные с web-сайта. Чтобы сделать это, используется метод OpenRead(), который возвращает ссылку типа Stream. Затем можно извлекать данные просто из потока.

WebClient Client = new WebClient();

Stream sfrm = Client.OpenRead("http://www.Wrox.com/default.htm");

Пример: базовый клиент Web

Первый пример продемонстрирует использование метода webClient.OpenRead(). В этом случае содержимое загруженных данных просто выводится в окне списка. Проект создается как стандартное приложение C# для Windows, в него добавляется окно списка с именем listBox1, в котором выводится содержимое загруженного файла. Затем в конструкторе основной формы делаются изменения:

public Form1() {

InitializeComponent();

System.Net.WebClient Client = new WebClient();

Stream strm = Client.OpenRead("http://www.wrox.com");

StreamReader sr = new StreamReader(strm);

string line;

do {

line = sr.ReadLine();

listBox1.Items.Add(line);

}

while (line != null) strm.Close();

}

Для упрощения URI в программе жестко закодирован.

Акроним URI (Uniform Resource Identifier) — Универсальный идентификатор ресурса — означает любую короткую строку, указывающую на некоторый ресурс. Следовательно, строка вида http://www.wrox.com является URI. В прошлом для идентификации таких адресов традиционно использовался термин URL (универсальный локатор ресурса), но термин URL больше не используется в новых технических спецификациях, теперь предпочтение отдается URI. URI имеет приблизительно такое же значение, как и URL, но более распространен, так как URI не обязательно предполагает, что используется один из известных протоколов, таких как HTTP или FTP.

Отметим, что в этом примере использованы два потока — StreamReader и соединенный с ним сетевой поток. Это обычно позволяет получать данные из потока как текст и использовать методы более высокого уровня, такие как ReadLine(), которые доступны в классе StreamReader. Это прекрасный пример сделанного в главе 14 замечания о достоинствах перехода от концепции перемещения данных к концепции потока. Выполнение примера создает следующий результат:

Выгрузка файлов

Класс WebClient обладает также методами UploadFile() и UploadData(). Различие между ними состоит в том, что UploadFile() выгружает указанный файл, заданный именем файла, в то время как UploadData() выгружает двоичные данные, которые поставляются как массив байтов:

WebClient client = new WebClient();

client.UploadData("http://www.ourwebsite.com/NewFile.htm", "C:\WebSiteFiles\NewFile.htm");

byte [] image;

// код для инициализации изображения, поэтому он содержит все

// двоичные данные для некоторого файла jpg

client.UploadData("http://www.ourwebsite.com/NewFile.jpg", image);

Классы WebRequest

Класс WebClient очень просто использовать, но он имеет ограниченные возможности. В частности, нельзя использовать его для предоставления полномочий аутентификации, так как существует не много сайтов, которые будут принимать выгружаемые файлы без аутентификации. Можно добавлять в запросы заголовочную информацию и проверять всю возвращаемую заголовочную информацию, но только в очень общем смысле, потому что нет специальной поддержки для какого-либо протокола. Причина этого состоит в том, что WebClient является классом общего назначения, созданным для работы с любым протоколом, для которого можно послать запрос и получить ответ (HTTP, FTP и т. д.). Он не может обрабатывать дополнительные свойства, специфические для какого-то одного протокола, такие как cookies, специфические для HTTP. Если желательно воспользоваться этими свойствами, необходимо выбрать семейство классов на основе двух других классов в пространстве имен System.Net: WebRequest и WebResponse.

Начнем с рассмотрения того, как загрузить страницу Web с помощью этих классов — тот же пример, что и раньше, но использующий WebRequest и WebResponse. В процессе изложения мы немного коснемся иерархии вовлеченных классов, а затем покажем, как воспользоваться дополнительными свойствами HTTP, поддерживаемыми этой иерархией.

Следующий код доказывает модификации, которые необходимо сделать в примере BasicWebClient, чтобы он использовал классы WebRequest и WebResponse.

public Form1() {

InitializeComponent();

WebRequest wrq = WebRequest.Create("http://www.wrox.com");

WebResponse wrs = wrq.GetResponse();

Stream strm = wrs.GetResponseStream();

StreamReader sr = new StreamReader(strm);

string line;

while ((line = sr.ReadLine()) != null) {

listBox1.Items.Add(line);

}

strm.Close();

}

Этот код начинается с создания экземпляра объекта, представляющий запрос Web. Необычно то, что это делается не с помощью использования конструктора, а с помощью вызова статического метода WebRequest.Create(); в следующем разделе будет показано, почему так это сделано. Класс WebRequest представляет запрос информации, который посылается по определенному URI, и поэтому необходимо передать URI с помощью метода Create(). WebResponse представляет данные, присылаемые назад сервером. Вызов метода WebRequest.GetResponse() в действительности посылает запрос серверу Web и создает объект Response, который можно использовать для проверки возвращаемых данных. Как и для объекта WebClient, можно получить поток, представляющий эти данные с помощью метода WebResponse.GetResponseStream().

Другие свойства WebRequest и WebResponse

Здесь кратко будут упомянуты пара других областей, в которых WebRequest и WebResponse и связанные классы предоставляют хорошую поддержку.

Информация заголовка HTTP

Важной частью протокола HTTP является возможность послать значительную заголовочную информацию с помощью потоков запроса и ответа. Эта информация может включать данные GET и POST, cookies, а также данные определенного пользователя, посылающего запрос. Как и можно было ожидать, предоставляется полная поддержка заданию и доступу к этим данным. Однако эта поддержка не является частью классов WebRequest и WebResponse, она реализована двумя производными классами: HttpWebRequest и HttpWebResponse. Как скоро будет показано, при создании объекта WebRequest с помощью обычного механизма, если предоставленный URI был URI HTTP, то получаемая ссылка в действительности указывает на объект HttpRequest, и можно при желании преобразовать эту ссылку в такой объект. Реализация HttpRequest метода GetResponse() возвращает объект HttpWebResponse через ссылку WebResponse, поэтому снова можно выполнить простое преобразование для доступа к свойствам, специфическим для HTTP.

Подробное описание этой области представлено в документации MSDN для классов HttpWebRequest и HttpWebResponse.

Асинхронные запросы страниц

Дополнительным полезным свойством WebRequest вместо WebClient является возможность асинхронного запроса страниц. Это важно, так как в Интернете возможна достаточно длительная задержка между отправкой запроса на хост и началом получения каких-либо данных. Методы, подобные WebClient.DownloadData и WebRequest.GetResponse, не возвращают управление, пока с сервера не вернется ответ. Может оказаться нежелательным, чтобы приложение было связано с ожиданием в течение этого времени. В таком случае попробуйте воспользоваться методами BeginGetResponse() и EndGetResponse(). Они работают асинхронно. Если вызвать BeginGetResponse(), то запрос будет отправлен на хост, а метод немедленно вернет управление, предоставляя делегата типа AsyncCallback. Пока сервер отвечает на запрос, можно будет выполнять другую работу. Подробное описание этих методов можно найти в MSDN.

Отображение выходных данных в виде страницы HTML

Первый пример показывает, что базовые классы .NET существенно облегчают загрузку и обработку данных из Интернета. Однако до сих пор файлы выводились только как простой текст. В приложении желательно часто просматривать файл HTML с помощью интерфейса в стиле Internet Explorer, чтобы установить, как действительно выглядит документ Web. К сожалению, когда писалась эта книга, базовые классы .NET не включали никакой собственной поддержки для управления этими свойствами интерфейса в стиле Internet Explorer. Чтобы сделать это, необходимо либо программным путем вызвать Internet Explorer, либо воспользоваться элементом управления ActiveX WebBrowser, который существовал уже до появления .NET.

Одним из случаев, где интерфейс пользователя выводится в стиле Internet Explorer, является создание приложения C#, которое генерирует или допускает редактирование страниц HTML с последующим выводом сгенерированных страниц пользователю.

Программный запуск процесса Internet Explorer, направленного на заданную страницу Web, можно выполнить с помощью класса Process из пространства имен System.Diagnostics.

Process myProcess = new Process();

myProcess.StartInfо.FileName = "iexplore.exe";

myProcess.StartInfo.Arguments = "http://www.wrox.com";

myProcess.Start();

Однако IE при этом запускается и как отдельное окно, которое на самом деле не соединено и не находится под управлением приложения. Следовательно, хотя этот код приведен здесь для справки, такая техника не подходит для частого применения.

С другой стороны использование элемента управления WebBrowser означает, что выведенный браузер может составить интегральную часть приложения, и приложение получит полный контроль над работой браузера. Этот элемент управления достаточно развит, обладает большим числом методов свойств и событий.

Простейшим способом встроить этот элемент управления с помощью Visual Studio.NET является добавление элемента управления в панель инструментов. Для этого щелкните правой кнопкой мыши на Toolbox в VisualStudio.NET и выберите Customize Toolbox из контекстного меню, что вызывает диалоговое окно, приведенное ниже. Необходимо выбрать вкладку COM Component и щелкнуть на Microsoft Web Browser.

Элемент управления WebBrowser появится теперь в панели инструментов, можно щелкнуть та нем, чтобы поместить его на форму в приложении Windows на C#, так же, как любой другой элемент управления Windows Forms в .NET. Visual Studio.NET автоматически создает весь код взаимодействия COM, необходимый программе C# для работы в качестве клиента с этим элементом управления, чтобы можно было интерпретировать его как просто элемент управления .NET. Это будет продемонстрировано с помощью другого очень короткого примера DisplayWebPage, в котором выводится страница Web с жестко закодированным URI.

DisplayWebPage создается как стандартное приложение Windows C#, элемент управления ActiveX WebBrowser помещается в форму, как описано выше. По умолчанию Visual Studio.NET именует соответствующую переменную axWebBrowser1, и здесь будет оставлено имя по умолчанию. Затем в конструктор Form1 добавляется код:

public Form1()

// Требуется для поддержки Windows Form Designer.

InitializeComponent();

int zero = 0;

object oZero = zero;

string emptyString = "";

object oEmptyString = emptyString;

axWebBrowser1.Navigate("http://www.wrox.com",

ref oZero, ref oEmptyString,

ref oEmptyString, ref oEmptyString);

}

В этом коде используется метод Navigate() элемента управления WebBrowser, который реально посылает запрос HTTP и выводит данные заданного URI. Первый параметр этого метода является строкой, содержащей URL, куда будет совершено перемещение. Остальные параметры соответственно позволяют: задать различные флажки; указать именованную рамку, в которой должен быть выведен браузер; определить любые данные POST, посылаемые с запросом, и дополнительную информацию заголовка HTTP. Используемых по умолчанию нулевых значений и пустых строк будет в данном случае достаточно. Эти параметры задаются в элементе управления как необязательные, но C# не поддерживает необязательные параметры, поэтому нужно определить их явно. Необходимо также явно объявить объектные ссылки для этих переменных, так как они передаются только по ссылке.

Вызов метода Navigate() с параметрами, как показано выше, имеет в принципе тот же результат, как и ввод URL в Internet Explorer для перехода к странице Web. Это единственный код, который необходимо добавить в проект DisplayWebPage вручную. При выполнении этого примера будет получен следующий результат (Visual Studio.NET также использовалась для изменения текста заголовка основной формы).

Отметим, что вывод страницы Web таким образом требует только унаследованного элемента управления WebBrowser, не нужно использовать никакие классы из пространства имен System.Net.

Иерархия классов WebRequest и WebResponse

В этом разделе более подробно рассматривается архитектура классов WebRequest и WebResponse.

Иерархия наследования вовлеченных классов показана на диаграмме.

Диаграмма показывает, что иерархия содержит более двух классов, которые используются в коде. Фактически оба класса WebRequest и WebResponse являются абстрактными, и поэтому нельзя создать экземпляры этих классов. Они существуют как базовые классы для предоставления общей функциональности для работы с запросами и ответами Web независимо от протокола, используемого в данной операции. Любой такой запрос всегда делается с помощью определенного протокола (HTTP, Telnet, FTP, SMTP и т. д.) и выполняется с помощью производного класса для этого протокола. В предыдущем коде, хотя ссылочные переменные были определены как ссылки на базовый класс, метод WebRequest.Create() в действительности давал объект HttpWebRequest, а метод GetResponse() возвращал объект HttpWebResponse тоже в действительности. Почему же этого не видно в коде явно? Ответ заключается в том, что компания Microsoft предоставила механизм на основе фабрики для сокрытия деталей иерархии классов от клиентского кода. Тот факт, что требуется объект, который может иметь дело с протоколом HTTP, очевиден из URI, подставляющего в WebRequest.Create(): http://www.wrox.com. WebRequest.Create() проверяет в URI спецификатор протокола и использует это для создания экземпляра и возврата объекта соответствующего класса. Цель состоит в том, чтобы никогда не использовать конструктор для создания экземпляра объекта WebRequest; таким образом появляется в некоторой степени свобода от необходимости что-либо знать о производных классах. Следует заметить, что теория немного отказывает, если нужно использовать какие-то специальные свойства используемого протокола, которые реализованы как методы производного класса, и в этом случае необходимо выполнить преобразование типа ссылки WebRequest или WebResponse в производный класс.

Теоретически с помощью этой архитектуры можно обрабатывать отправку запросов с помощью любого из распространенных протоколов. Однако компания Microsoft в действительности написала производные классы, охватывающие протоколы HTTP и file://. Если желательно иметь дело с другими протоколами, например, FTP, Telnet или SMTP, то нужно либо воспользоваться API Windows и написать свои собственные классы (которые будут внутренне реализованы с помощью Windows API), или ждать, пока независимые поставщики программного обеспечения напишут подходящие классы .NET.

Служебные классы

В этом разделе будет представлена пара служебных классов, которые могут облегчить программирование Web при работе с двумя распространенными темами Интернета — URI и адреса IP.

URI

Uri и UriBuilder являются двумя классами в пространстве имен System (подчеркнем, что не System.Net) и оба предназначены для представления Uri. Различие между ними состоит в том, что UriBuilder создает URI по заданным строкам для его компонентов, в то время как Uri выполняет синтаксический разбор всего URI.

Поскольку класс Uri предназначен скорее для разбиения, а не для создания Uri, он требует полной строки URI, чтобы ее создать.

Uri MSPage =

new Uri("http://www.Microsoft.com/SomeFolder/SomeFile.htm?Order=- true");

Класс представляет большое число свойств, которые предназначены только для чтения. В объекте Uri не предусмотрены изменения после того, как он был создан.

string Query = MSPage.Query;               // Order = true

string AbsolutePath = MSPage.AbsolutePath; // SomeFolder/SomeFile.htm

string Schema = MSPage.Schema;             // http

int Port = MSPage.Port;                    // 80 (по умолчанию для http)

string AbsolutePath = MSPage.AbsolutePath; // SomeFolder/SomeFile.htm

string Host = MSPage.Host;                 // www.Microsoft.com

bool IsDefaultPort = MSPage.IsDefaultPort; // true, так как

// 80 — значение по умолчанию

UriBuilder, с другой стороны, реализует меньше свойств: лишь столько, чтобы построить полный URI. Однако эти свойства предназначены для чтения-записи. Можно задать компоненты для создания URI с помощью конструктора:

Uri MSPage =

new UriBuilder("http", "www.Microsoft.com", 80,

"SomeFolder/SomeFile.htm", "Order=true");

Или присвоить значения свойствам.

UriBuilder MSPage = new UriBuilder();

MSPage.Schema = "http";

MSPage.Host = "www.Microsoft.com";

MSPage.Port = 80;

MSPage.Path = "SomeFolder/SomeFile.htm";

MSPage.Query = "Order=true";

После завершения инициализации UriBuilder получается соответствующий объект Uri со свойством Uri.

Uri CompletedUri = MSPage.Uri;

Пример вывода страницы

Мы проиллюстрируем использование UriBuilder вместе с созданием процесса Internet Explorer на примере DisplayPage. Этот пример позволяет пользователю ввести компоненты URL. Отметим, что имеется в виду URL, а не URI, так как предполагается, что это запрос HTTP. Пользователь сможет затем щелкнуть на кнопке ViewPage, и приложение выведет весь URL в текстовом поле, а также страницу с помощью элемента управления ActiveX WebBrowser.

Этот пример, будучи стандартным приложением Windows на C#, выглядит так:

Текстовые поля названы соответственно textBoxServer, textBoxPath, textBoxPort и textBoxURI. Добавленный код примера полностью находится в обработчике событий кнопки ViewPage:

private void OnClickViewPage(object sender, System.EventArgs e) {

UriBuilder Address = new UriBuilder();

Address.Host = textBoxServer.Text;

Address.Port = int.Parse(textBoxPort.Text);

Address.Scheme = Uri.UriSchemeHttp;

Address.Path = textBoxFile.Text;

Uri AddressUri = Address.Uri;

Process myProcess = new Process();

myProcess.StartInfo.FileName = "iexplorer.exe";

textBoxURI.Text = AddressUri.ToString();

myProcess.StartInfo.Arguments = AddressUri.ToString();

myProcess.Start();

}

Адреса IP и имена DNS

Серверы в Интернете, так же, как и клиенты, идентифицируются адресами IP или по именам хостов (называемых также именами DNS). Обычно имя хоста, такое как www.wrox.com или www.microsoft.com, является понятным для пользователя и вводится в окне браузера Web. Адрес IP, с другой стороны, является идентификатором, который компьютеры используют для идентификации друг друга, и реально используется для обеспечения того, чтобы запросы и ответы Web направлялись на соответствующие машины.

Адрес IP является просто 32-битовым целым числом, которое обычно представляют в так называемом десятичном формате с точками как набор из четырех чисел — каждое между 0 и 255. Компьютер может иметь даже более одного адреса IP. Например, IP 100.100.100.31 при общении с другими компьютерами в Интернете через модем и адрес 10.0.0.1 при общении через свою сетевую карту с другими компьютерами в локальной сети. Следовательно, если другие компьютеры в локальной сети хотят послать сообщение этому компьютеру, они должны обращаться по адресу 10.0.0.1, в то время как компьютеры в Интернете должны обращаться к нему по адресу 100.100.100.31. Адреса обычно динамически присваиваются компьютеру каждый раз либо при загрузке (для компьютеров с постоянным соединением с интранет), либо при установлении связи (для компьютеров, соединенных с провайдером Интернета через модем или аналогичное устройство). Динамически присваиваемые адреса IP передаются компьютеру по мере необходимости из пула адресов, поддерживаемых его провайдером Интернета.

Люди редко вводят адреса IP непосредственно, а используют более понятные имена DNS, такие как www.wrox.com. Поэтому необходимо при отправке сетевого запроса сначала транслировать имя DNS в адрес IP, что выполняет один из нескольких серверов DNS.

Это организовано следующим образом: некая машина, соединенная с локальным компьютером (что означает либо выделенную машину в локальной сети, либо при модемном соединении машину в корпоративной сети, с которой происходит соединение), будет определена сервером DNS. Эта машина хранит огромную таблицу, отображающую имена DNS в адреса IP для всех известных ему компьютеров, и также содержит адреса IP других серверов DNS, имеющие возможность найти адреса, о которых она не знает. Каждый сетевой компьютер должен знать адрес IP по крайней мере одного локального сервера DNS (это один из элементов данных, которые необходимо предоставить при настройке сетевой конфигурации на компьютере). Когда посылается запрос, компьютер сначала запрашивает сервер DNS, чтобы он сообщил адрес IP, соответствующий введенному адресу домена. Затем, с помощью правильного адреса IP, направляется запрос. Обычно это происходит неявно и поэтому прозрачно, например при перемещении в Интернете.

Классы .NET для адресов IP

.NET предоставляет ряд классов, которые могут помочь в процессе поиска адресов IP и при получении информации о компьютерах-хостах.

IPAddress

Класс IPAddress представляет адрес IP, который доступен как свойство Address и может быть преобразован в десятичный формат с точками с помощью ToString(). IPAddress реализует также статический метод Parse(), эффективно выполняя обратное преобразование в ToString() — из десятичной строки с точками в (целочисленный) адрес IP.

IPAddress ipAddress = IPAddress.Parse("234.56.78.9");

int address = ipAddress.Address;        // address будет присвоено 37105130

string ipString = ipAddress.ToString(); // ipString будет присвоен

// текст "234.45.54.2"

IPAddress предоставляет также ряд константных статических полей, которые возвращают известные специальные адреса IP, имеющие специальные значения.

//следующая строка задает обратную петлю как "127.0.0.1"

// адрес обратной петли указывает локальный хост

string loopback = IPAddress.Loopback.ToString();

// следующая строка задает адрес широковещания как "255.255.255.255"

// адрес широковещания используется для отправки сообщения

// всем машинам в локальной сети

String broadcast = IPAddress.Broadcast.ToString();

IPHostEntry

Класс IPHostEntry инкапсулирует информацию, связанную с определенным хостом (компьютером). Он делает доступным имя хоста с помощью свойства HostName (которое возвращает строку) и все адреса IP с помощью свойства AddressList, которое возвращает массив объектов IPAddress. Класс IPHostEntry будет показан в действии в примере DNSResolver ниже.

DNS

Класс DNS является классом, который может общаться с используемым по умолчанию сервером DNS, чтобы извлечь адреса IP. Двумя важными (статическими) методами здесь являются Resolve(), использующий сервер DNS для получения данных хоста с заданным именем хоста, и GetHostByAddress(), который также посылает назад эти данные, но в этот раз с помощью адреса. Оба метода возвращают объект IPHostEntry.

HostEntry wroxHost = Dns.Resolve("www.microsoft.com");

HostEntry wroxHostCopy = Dns.GetHostByAddress("234.234.234.234");

В этом коде оба объекта HostEntry будут содержать данные серверов Microsoft.com.

Класс Dns отличается от классов IPAddress и IPHostEntry тем, что инкапсулирует возможность реально общаться с серверами для получения информации. Классы IPAddress и IPHostEntry, напротив, мало чем отличаются от структур данных, которые имеют подходящие свойства, позволяющие получить доступ к содержащимся в них данным.

Пример: DnsLookup

Связанные с DNS и IP классы будут проиллюстрированы с помощью примера, который ищет имена DNS. Этот снимок экрана показывает DnsLookup в действии.

Пример просто предлагает пользователю ввести имя DNS в основное текстовое поле. Когда пользователь нажимает кнопку Resolve, пример использует метод Dns.Resolve() для извлечения ссылки IPHostEntry и выводит имя хоста и адреса IP. Отметим, что выведенное имя хоста может в некоторых случаях отличаться от введенного имени. Это происходит, если одно имя DNS (www.microsoft.com) действует просто как прокси для другого имени DNS (www.microsoft.akadns.net).

Приложение DnsLookup является стандартным оконным приложением C# с элементами управления, как показано на снимке экрана, и присвоенными им соответственно именами textBoxInput, buttonResolve, textBoxHostName и listboxIPs. Затем в класс Form1 добавляется следующий метод заданием его в окне свойств Visual Studio.NET в качестве обработчика событий для нажатия кнопки buttonResolve.

void OnResolve(object sender, EventArgs e) {

try {

IPHostEntry iphost = Dns.Resolve(textBoxInput.Text);

foreach(IPAddress ip in iphost.AddressList) {

int ipaddress = ip.Address;

listBoxIPs.Items.Add(ipaddress);

listBoxIPs.Items.Add(" " + Dns.IpToString(ipaddress));

}

textBoxHostName.Text = iphost.HostName;

} catch(Exception ex) {

MessageBox.Show("Unable to process the request because " +

"the following problem occurred:\n" + ex.Message,

"Exception occurred");

}

}

Отметим, что в этом коде перехватываются все исключения — исключение может легко возникать, если пользователь вводит что-то, что не является именем DNS, или если сеть выключена.

После извлечения экземпляра IPHostEntry используется свойство AddressList для получения массива, содержащего адреса IP, которые затем перечисляются в цикле foreach. Каждый адрес выводится как целое число и как строка использованием статического метода Dns.IpToString(), который делает то же самое, что и экземпляр метода IPAddress.ToString().

Протоколы нижнего уровня

В этом разделе кратко упоминаются некоторые из классов .NET, используемые для коммуникации на нижнем уровне.

Коммуникация между компьютерами работает на нескольких различных уровнях. Рассмотренные до сих пор в этой главе классы действуют на самом высоком уровне, на котором определены особые команды. Наверное, проще всего понять это, рассматривая FTP, так как многие разработчики знакомы с командами FTP. Хотя в последние годы появился ряд хороших утилит FTP на основе UI, но до недавних пор основным инструментом, используемым для FTP в среде Windows, была команда DOS ftp, которая работала в командной строке, и поэтому использование ее включало явный ввод посылаемых серверу инструкций ftp.

HTTP, Telnet, SMTP, POP и другие протоколы основываются на похожих наборах команд. Единственное различие состоит в том, что для большинства из этих протоколов используются инструменты, которые скрывают передачу команд от пользователя, так что он обычно об этом не знает. Например, когда в браузере Web вводится URL и посылается запрос Web, браузер на самом деле посылает на сервер команду GET (обычным текстом), соответствующую по своему назначению команде FTP get. Он может также послать команду POST, которая указывает, что к запросу присоединены другие данные.

Однако эти протоколы сами по себе не являются достаточными для обеспечения коммуникации между компьютерами. Даже если клиент и сервер понимают, например, протокол HTTP, они все равно не смогут понять друг друга, если не будет точно согласовано, как передавать символы. Какой двоичный формат будет использоваться, и, даже спускаясь на самый нижний уровень, какое напряжение будет использоваться для представления 0 и 1 в двоичных данных? Это связано с тем, что существует множество различных согласуемых протоколом элементов, которые разработчики и инженеры, работающие в этой области, часто называют стеком протоколов. Стек протоколов является, по сути, списком различных протоколов от самого верхнего уровня (HTTP, FTP и т. д.) до базовых протоколов напряжения нижнего уровня и т.д.

К счастью, для большей части разработок нет необходимости спускаться так далеко вниз по стеку. Но если требуется эффективная коммуникация, нет ничего необычного в написании кода, который работает непосредственно на уровне пересылки между компьютерами пакетов двоичных данных. Это область таких протоколов, как TCP, и Microsoft предоставляет ряд классов, которые позволяют удобно работать с двоичными данными на этом уровне. Используя эти классы, можно, например, послать широковещательное сообщение, которое будет одновременно получено рядом компьютеров в сети.

Классы нижнего уровня

Соответствующие классы определены в пространстве имен System.Net.Sockets и позволяют, например, посылать непосредственно запросы TCP или ожидать запросы TCP на определенном порте. Основными классами нижнего уровня являются:

Класс

Назначение

Socket

Низкоуровневый класс, который имеет дело с реальным управлением соединениями. Этот класс используется внутренне такими классами, как WebRequest и TcpClient.

NetworkStream

Производный класс из Stream. Представляет поток данных из сети.

TcpClient

Позволяет создать соединения для приемников.

TcpListener

Позволяет ожидать входящие запросы соединения.

UdpClient

Позволяет создать соединения для клиентов UDP. (UDP является альтернативным протоколом для TCP, но используется значительно менее широко, в основном в локальных сетях.)

Заключение

В этой главе очень кратко были рассмотрены некоторые из базовых классов .NET, которые имеют дело с открытием клиентских соединений в сети и Интернете, а также отправкой запросов и получением ответов от серверов. Наиболее очевидным результатом этого будет получение страниц HTML.

Классы .NET во время написания этой книги по общему признанию слабо охватывали своей поддержкой многие распространенные протоколы, и примечательно, что даже для выполнения такой базовой задачи, как вывод страницы HTML пришлось обращаться к унаследованному элементу управления ActiveX WebBrowser. (Необходимо сказать, что это не является таким уж большим недостатком, поскольку возможность взаимодействия .NET COM и Visual Studio.NET делают использование подобных элементов управления почти таким же простым, как использование собственных элементов управления Windows Forms). Тем не менее доступные классы .NET делают выполнение некоторых важных задач простыми по своей сути, это особенно справедливо для процесса помещения страницы Web в поток или файл.

Главa 23

Создание распределенных приложений с помощью .NET Remoting

В главе 17 были рассмотрены службы Web, которые позволяют вызывать объекты на удаленном сервере. Использование сервера Web и протокола SOAP не всегда достаточно эффективно для приложений интранет. Протокол SOAP означает большие накладные расходы при пересылке большого объема данных. Для быстрых решений интранет можно использовать просто сокеты, как это делалось в предыдущей главе. В "старом мире", как известно, программы писали с использованием DCOM. С помощью DCOM можно вызывать методы на объектах, выполняющихся на сервере. Программная модель всегда является одной и той же, если объекты применяются на сервере или на клиенте.

Без DCOM мы вынуждены иметь дело с портами и сокетами, уделяя внимание целевым платформам в связи с возможным различным представлением данных, и создавать специальные протоколы, в которых сообщения посылаются сокету, чтобы, в конце концов, вызвать некоторые методы. DCOM обрабатывает все эти вопросы для программиста.

Заменой DCOM является .NET Remoting. В противоположность DCOM .NET Remoting может использоваться также в решениях Интернета, а для них DCOM недостаточно гибок и эффективен. С помощью .NET Remoting можно адаптировать и расширить любую часть архитектуры, поэтому подходит практически для любого удаленного сценария. В этой главе будут рассмотрены:

□ Архитектура .NET Remoting

□ Каналы, сообщения, приемники

□ Создание клиентов и серверов

□ Удаленные свойства с конфигурационными файлами

□ Возможности расширения рабочей среды

□ Использование возможностей удаленного управления в приложениях ASP.NET

Прежде всего выясним, что такое .NET Remoting.

Что такое .NET Remoting

Два выражения могут описать .NET Remoting: Web Services Anywhere и CLR Object Remoting. Рассмотрим, что это означает.

Web Services Anywhere

Выражение Web Services Anywhere используется в .NET Remoting и означает, что с помощью .NET Remoting службы Web могут применяться в любом приложении с помощью любого транспорта, используя какое угодно кодирование полезной нагрузки. .NET Remoting является предельно гибкой архитектурой.

Совместное использование SOAP и HTTP — только способ вызова удаленных объектов. Транспортный канал является подключаемым и может заменяться. Мы получаем каналы HTTP и TCP, представленные классами HttpChannel и TcpChannel. Мы вправе создать транспортные каналы для использования UDP, IPX или механизма общей памяти — выбор полностью зависит от программиста.

Кодирование полезной нагрузки также можно заменить. Компания Microsoft предоставляет SOAP и механизмы двоичного кодирования. Можно использовать средство форматирования (форматтер) SOAP с помощью канала НТTР, но и использовать HTTP, применяя двоичный форматтер. Конечно оба эти форматтера можно использоватъ также с каналом TCP.

.NET Remoting не только делает возможным использование служб Web в каждом приложении .NET, но позволяет также предложить возможности службы Web в каждом приложении. Не имеет значения, создается ли консольное приложение или приложение для Windows, Windows Service или компонент COM+ — службы Web могут использоваться везде.

Термин подключаемый (pluggable) часто используется в .NET Remoting.

Подключаемый означает, что определенная часть проекта разработана так, что она может заменяться специальной реализацией.

CLR Object Remoting

CLR Object Remoting везде располагается поверх служб Web. CLR Object Remoting облегчает использование служб Web. Все конструкции языка, такие как конструкторы, делегаты, интерфейсы, методы, свойства и поля, могут использоваться с удаленными объектами. CLR Object Remoting имеет дело с активацией, распределенной идентификацией, временем жизни и контекстами вызова.

Обзор .NET Remoting

.NET Remoting может использоваться для доступа к объектам в домене другого приложения независимо от того, находятся ли два объекта внутри одного процесса, в разных процессах или на разных системах.

Удаленные сборки можно сконфигурировать для локальной работы в домене приложения или как часть удаленного приложения. Если сборка является частью удаленного приложения, то клиент получает для общения прокси вместо реального объекта. Прокси посылает сообщение в канал.

Приложения .NET работают внутри домена приложения. Домен приложения можно рассматривать как подпроцесс внутри процесса, Традиционно процессы используются как изолирующая граница. Приложение, выполняющееся в одном процессе, не может получить доступ и разрушить память в другом процессе. Чтобы приложения общались друг с другом, требуется межпроцессная коммуникация. При использовании .NET домен приложения выступает новой границей безопасности внутри процесса, так как код CIL является проверяемым и обеспечивает безопасность типов данных. Различные приложения могут выполняться внутри одного процесса, но внутри различных доменов приложений. Объекты внутри одного домена приложений могут взаимодействовать напрямую. Чтобы получить доступ к объектам в другом домене приложений, требуется прокси.

Больше о доменах приложений можно узнать в главе 8.

Прежде чем перейти к внутренней функциональности .NET Remoting, давайте рассмотрим основные элементы архитектуры:

□ Удаленный объект является объектом, который выполняется на сервере. Клиент не вызывает методы на этом объекте напрямую, а использует для этого прокси. С помощью .NET легко отличить удаленные объекты от локальных: каждый класс, производный из MarshalByValueObject, никогда не покидает свой домен приложений. Клиент может вызывать методы на удаленном объекте через прокси.

□ Канал используется для коммуникации между клиентом и сервером. Существует клиентская и серверная часть канала. С помощью .NET Framework мы получаем два типа каналов, которые общаются через TCP или HTTP. Можно также создать специальный канал, который поддерживает связь с помощью другого протокола.

□ Сообщения посылаются в канал. Они создаются для коммуникации между клиентом и сервером и хранят информацию об удаленных объектах, именах вызванных методов и всех аргументах.

□ Форматтер определяет, как сообщения передаются в канал. Вместе с .NET Framework мы получаем форматтеры SOAP и двоичный. Форматтер SOAP можно использовать для коммуникации со службами Web, которые не основываются на .NET Framework. Двоичные форматтеры действуют значительно быстрее и могут эффективно использоваться в среде интранет. Конечно, имеется возможность создать специальный форматтер.

□ Провайдер форматтера используется для соединения форматтера с каналом. Создавая канал, можно выбрать провайдер форматтера, и этот выбор в свою очередь, определяет форматтер, который будет использоваться для передачи данных в канал.

□ Клиент вызывает методы на прокси, а не на удаленном объекте. Существует два типа прокси: прозрачный прокси и реальный прокси. Прозрачный прокси выглядит для клиента как удаленный объект. Клиент может вызывать методы, реализуемые удаленным объектом на прозрачном прокси. В свою очередь, прозрачный прокси вызывает метод Invoke() на реальном прокси. Метод Invoke() использует приемник сообщений для передачи сообщения в канал.

□ Приемник сообщений является объектом-перехватчиком. Такие перехватчики имеются как на клиенте, так и на сервере. Приемник ассоциируется с каналом. Реальный прокси использует его для передачи сообщения в канал, поэтому приемник осуществляет некоторый перехват, прежде чем сообщения попадают в канал.

□ Клиент может использовать активатор для создания удаленного объекта на сервере или для получения прокси активированного сервером объекта.

□ RemotingConfiguration является служебным классом для конфигурирования удаленных серверов и клиентов. Этот класс используется для чтения конфигурационных файлов или для динамического конфигурирования удаленных объектов.

□ ChannelServices является служебным классом для регистрации каналов и затем — для отправки сообщений в канал.

Чтобы получить представление о функциональности, давайте рассмотрим концептуально, как элементы сочетаются друг с другом.

Когда клиент вызывает методы на удаленном объекте, он на самом деле вызывает вместо этого методы на прозрачном прокси. Прозрачный прокси выглядит как реальный объект, он реализует открытые методы реального объекта. Прозрачный прокси узнает об открытых методах, используя механизм отражения для считывания метаданных из сборки.

Прозрачный прокси, в свою очередь, вызывает реальный прокси. Реальный прокси отвечает за отправку сообщения в канал. Реальный прокси является подключаемым, можно заменить его с помощью специальной реализации, которая применяется для записи журнала для другого способа поиска канала и т.д. Используемая по умолчанию реализация реального прокси находит совокупность (или цепочку) уполномоченных приемников и передает сообщение в первый уполномоченный приемник. Уполномоченный приемник может перехватить и изменить сообщение. Примерами таких приемников являются приемники отладки, системы безопасности, синхронизации. Последний уполномоченный приемник досылает сообщение в канал. Как сообщение передается по линиям связи, зависит от форматтера. Ранее уже упоминалось, что имеются SOAP и двоичный форматтеры, которые также являются подключаемыми. Канал отвечает либо за соединение с принимающим сокетом на сервере, либо за отправку форматированных данных. Со специальным каналом можно производить различные действия, необходимые для передачи данных на другую сторону.

Продолжим рассмотрение на серверной стороне. Канал получает форматированные сообщения от клиента и использует форматтер для демаршализации данных SOAP или двоичных данных в сообщениях. Затем канал вызывает приемники серверного контекста, которые, в свою очередь, являются цепочкой приемников, где последний приемник в цепочке продолжает вызов в цепочке объектов приемников контекста. Последний объект приемника контекста вызывает метод в удаленном объекте. Объектные приемники соответствуют объекту, серверные приемники контекста соответствуют контексту. Для доступа к ряду объектных приемников может использоваться единственный приемник контекста

.NET Remoting легко модифицируется: можно заменить реальный прокси, добавить объекты приемника, заменить форматтер и канал. Конечно, можно также использовать все, что уже предоставлено. При этом необходимо отметить, что связанные с прохождением через эти слои накладные расходы можно считать почти отсутствующими. Если вы добавите собственную функциональность, накладные расходы будут зависеть от нее.

Контексты

Прежде чем рассматривать возможности .NET Remoting для создания серверов и клиентов, которые общаются в сети, давайте рассмотрим те случаи, когда внутри домена приложения требуется канал, осуществляющий вызов объектов через контексты.

При создании компонентов COM+ использовались контексты COM+. Контексты в .NET являются очень похожими. Как уже было сказано, один процесс может иметь несколько доменов приложений. Домен приложения является чем-то типа подпроцесса с границами безопасности и может иметь различные контексты. Контекст используется для группирования объектов с аналогичными требованиями выполнения. Контексты состоят из множества свойств и используются для перехватывания: когда к ограниченному контекстом объекту обращаются из другого контекста, перехватчик может сделать некоторую работу, прежде чем вызов достигнет объекта.

Класс, который выводится из MarsnalByRefObject, ограничен доменом приложения. Вне домена приложения требуется прокси для доступа к объекту. Класс, который выводится из ContextBoundObject ограничен контекстом. Вне контекста для доступа к объекту требуется прокси. Ограниченные контекстом объекты могут иметь атрибуты контекста, объект без таких атрибутов создается в контексте создателя. Ограниченный контекстом объект с атрибутами контекста создается в новом контексте или в контексте создателя, если атрибуты являются совместимыми

Чтобы понять контексты, необходимо знать некоторые термины:

□ Создание домена приложения приводит к возникновению контекста по умолчанию в этом домене. Если вы возьмете экземпляр нового объекта, которому требуются другие свойства контекста, в соответствии с ними создастся новый контекст.

□ Атрибуты контекста могут присваиваться классам, производным из ContextBoundObject. Можно создать класс специального атрибута, реализуя интерфейс IContextAttribute. .NET Framework имеет два класса атрибутов контекста: SynchronizationAttribute и ThreadAffinityAttribute.

□ Атрибуты контекста определяют свойства контекста, необходимые объекту. Класс свойства контекста реализует интерфейс IContextProperty. Активные свойства предоставляют приемники сообщений в цепочку вызовов. Класс ContextAttribute, который может использоваться как базовый для специальных атрибутов, реализует как IContextProperty, так и IContextAttribute.

□ Приемник сообщений является перехватчиком вызова метода. При этом свойства помогают работе приемников сообщений.

Активизация

Новый контекст создается, если экземпляру создаваемого класса требуется контекст, отличный от вызывающего контекста. Классы атрибутов, которые ассоциируются с целевым классом, запрашиваются в том случае, если все свойства текущего контекста в порядке. Если один из этих классов атрибутов присылает false, то среда выполнения запрашивает все классы свойств, связанные с классом атрибута, и создает новый контекст. Среда выполнения запрашивает затем у классов свойств о приемниках, которые они хотят установить. Класс свойства может реализовать интерфейсы IContributeXXXSink для содействия объектам приемника.

Атрибуты и свойства

Класс атрибута контекста является прежде всего атрибутом. Более подробно можно прочитать об этом в главе 6. Классы атрибутов контекста должны реализовать интерфейс IContextAttribute. Специальный класс атрибута контекста можно вывести из класса ContextAttribute, так как этот класс уже имеет используемую по умолчанию реализацию данного интерфейса.

В .NET Framework содержатся два класса атрибутов контекста: System.Runtime.Remoting.Contexts.SynchronizationAttribute и System.Runtime.Remoting.Contexts.ThreadAffinityAttribute. С помощью атрибута ThreadAffinity можно задать, что только один поток выполнения получает доступ к полям экземпляра и методам ограниченного контекстом класса. Это полезно для объектов интерфейса пользователя, так как дескрипторы окон определяются относительно потока выполнения. Атрибут Synchronization, с другой стороны, определяет требования синхронизации. Здесь можно задать, что несколько потоков выполнения не вправе получить доступ к объекту одновременно, но поток выполнения, получающий доступ к объекту, может меняться.

С помощью этих атрибутов в конструкторе задаются четыре значения:

□ NOT_SUPPORTED определяет, что экземпляр класса не должен создаваться в контексте, который имеет либо сходство с потоком выполнения, либо с множеством синхронизации.

□ REQUIRED устанавливает, что требуется контекст со сходством с потоком выполнения/синхронизацией.

□ REQUIRES_NEW всегда обеспечивает получение нового контекста.

□ SUPPORTED означает, что независимо от того, какой контекст мы получаем, объект сможет в нем существовать.

Коммуникация между контекстами

Как же происходит коммуникация между контекстами? Клиент использует вместо реального объекта прокси. Оно создает сообщение, которое передается в канал, и приемники могут выполнить перехват. Тот же самый механизм используется для коммуникации между различными доменами приложения или различными системами. Канал TCP или HTTP не требуется для коммуникации между контекстами, но канал здесь, конечно же, есть. Класс CrossContextChannel может использовать одну и ту же виртуальную память на клиентской и на серверной стороне канала, при этом для соединения контекстов не требуются форматтеры.

Удаленные объекты, клиенты и серверы

Прежде чем перейти к рассмотрению деталей архитектуры .NET Remoting, давайте рассмотрим кратко удаленный объект и очень маленькое простое клиентское серверное приложение, которое использует этот удаленный объект. Затем мы обсудим более подробно все необходимые шаги и параметры.

Реализуемый удаленный объект называется Hello. HelloServer является основным классом приложения на сервере, a HelloClient предназначен для клиента:

Удаленные объекты

Удаленные объекты требуются для распределенного вычисления. Объект, вызываемый удаленно с другой системы, выводится из объектов System.MarshalByRefObject.MarshalByRefObject и соответствует домену приложения, в котором они создаются. Это означает, что такие объекты никогда не переходят между доменами приложений; вместо этого используется объект прокси для доступа к удаленному объекту изнутри другого домена приложения. Другой домен приложения может существовать внутри того же процесса, другого процесса, или на другой системе.

Удаленный объект имеет распределенную идентичность. В связи с этим ссылка на объект может передаваться другим клиентам, и они также будут получать доступ к тому же объекту. Прокси знает об идентичности удаленного объекта.

Класс MarshalByRefObject имеет в дополнение к унаследованным методам из класса Object методы для инициализации и получения служб времени жизни. Службы времени жизни определяют, как долго живут удаленные объекты. Службы времени жизни и арендуемые свойства будут рассмотрены позже в этой главе.

Чтобы увидеть .NET Remoting в действии, создается простая библиотека классов для удаленного объекта. Класс Hello выводится из System.MarshalByRefObject. В конструкторе и деструкторе на консоли записывается сообщение, чтобы было известно о времени жизни объекта. Кроме того, имеется только один метод Greeting(), который будет вызываться от клиента.

Для того чтобы легко различать в последующих разделах сборку и класс, дадим им различные имена аргументов, которые используют вызовы метода. Присвоим сборке имя RemoteHello.dll, а классу — Hello. Типом проекта Visual Studio.NET, используемым для этого класса, является Visual C# Class Library:

namespace Wrox.ProfessionalCSharp {

using System;

/// <summary>

/// Краткое описание Class1

/// </summary>

public class Hello: System.MarshalByRefObject {

public Hello() {

Console.WriteLine("Constructor called");

}

~Hello() {

Console.WriteLine("Destructor called");

}

public string Greeting(string name) {

Console.WriteLine("Greeting called");

return "Hello, " + name;

}

}

}

Простой сервер

Для сервера используется консольное приложение C#. Для применения класса TcpServerChannel необходимо сослаться на сборку System.Runtime.Remoting.dll. Также требуется, чтобы мы ссылались на созданную ранее сборку RemoteHello.dll.

В методе Main() создается объект System.Runtime.Remoting.Channel.Тcр.TcpServerChannel с портом номер 8086. Этот канал регистрируется в классе System.Runtime.Remoting.Channels.ChannelServices, чтобы сделать его доступным для удаленных объектов. Тип удаленного объекта регистрируется с помощью System.Runtime.Remoting.RemotingConfiguration.RegisterWellKnownServiceType. Здесь определяется тип класса в удаленном объекте, используемый клиентом URI, и режим. Режим WellKnownObject.SingleCall означает, что для каждого вызова метода создается новый экземпляр, мы не храним состояние в удаленном объекте.

После регистрации удаленного объекта продолжим выполнение сервера, пока не будет нажата клавиша:

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Tcp;

namespace Wrox.ProfessionalCSharp {

/// <summary>

/// Краткое описание Class1

/// </summary>

public class HelloServer {

public static void Main(string[] args) {

TcpServerChannel channel = new TcpServerChannel(8086);

ChannelServices.RegisterChannel(channel);

RemotingConfiguration.RegisterWellKnownServiceType(

typeof(Hello), "Hi", WellKnownObjectMode.SingleCall);

System.Console.WriteLine("hit to exit");

System.Console.ReadLine();

}

}

}

Простой клиент

Клиент также является консольным приложением C#. И здесь делается ссылка на сборку System.Runtime.Remoting.dll, чтобы можно было использовать класс TcpClientChannel. Кроме того, имеется ссылка на сборку RemoteHello.dll. Хотя объект будет создаваться на удаленном сервере, нам понадобится сборка на стороне клиента, чтобы прокси прочитал метаданные во время выполнения.

В клиентской программе создается объект TcpClientChannel, который регистрируется в ChannelServices. Для TcpChannel используется конструктор по умолчанию, поэтому выбирается свободный порт. Затем используется класс Activator для возврата прокси удаленному объекту. Прокси является типом System.Runtime.Remoting.Proxies._TransparentProxy. Этот объект выглядит как реальный. Это делается с помощью механизма отражения, в котором считываются метаданные реального объекта. Прозрачный прокси использует реальный для пересылки сообщений в канал:

using System;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Tcp;

namespace Wrox.ProfessionalCSharp {

/// <summary>

/// Краткое описание Class1.

/// </summary>

public class HelloClient {

public static void Main(string[] args) {

ChannelServices.RegisterChannel(new TcpClientChannel());

Hello obj = (Hello)Activator.GetObject(typeof(Hello), "tcp://localhost:8086/Hi");

if (obj == null) {

Console.WriteLine("could not locate server"); return;

}

for (int i = 0; i < 5; i++) {

Console.WriteLine(obj.Greeting("Christian"));

}

}

}

}

Когда запустятся сервер и клиентская программа Hello, Christian появится пять раз на клиентской консоли. В консольном окне серверного приложения можно будет увидеть вывод, аналогичный следующему:

Первый конструктор вызывается во время регистрации удаленного объекта. Как утверждалось ранее, метод RemotingConfiguration.RegisterWellKnownServiceType() уже создает один экземпляр. Затем для каждого вызова метода создается новый экземпляр, так как был выбран режим активации WellKnownObjectMode.SingleCall. В зависимости от синхронизации и необходимых ресурсов, будут наблюдаться также вызовы деструктора. Если запустить клиент несколько раз, то вызовы деструктора будут присутствовать наверняка.

Архитектура .NET Remoting

Показав в действии простой клиент, сервер и изучив архитектуру .NET, перейдем к деталям. На основе созданных ранее программ будут рассмотрены механизмы архитектуры и способы расширения.

Каналы

Канал используется для коммуникации между клиентом .NET и сервером. Среда .NET поставляется с классами каналов, которые общаются с помощью TCP или HTTP. Можно создать специальные каналы для других протоколов.

Канал HTTP применяется большинством служб Web. Он использует для коммуникации протокол HTTP, так как брандмауэры обычно имеют открытым порт 80, чтобы клиенты могли получить доступ к серверам и службам Web. Прием на порту 80 также находится в распоряжении этих клиентов.

В Интернете используется и канал TCP, но здесь брандмауэры должны специально конфигурироваться, чтобы клиенты получали доступ к указанному порту канала TCP. Канал TCP по сравнению с HTTP может применяется для коммуникации более эффективно в интранет.

Когда выполняется вызов метода на удаленном объекте, объект клиентского канала посылает сообщение удаленному объекту канала.

Как серверное, так и клиентское приложения, должны создавать канал. Следующий код показывает, как можно создать TcpServerChannel на серверной стороне:

using System.Runtime.Remoting.Channels.Tcp;

// ...

TcpServerChannel channel = new TcpServerChannel(8086);

Порт, который слушает сокет TCP, определяется аргументом конструктора. Серверный канал должен определить общеизвестный порт, а клиент использует его порт при доступе к серверу. Однако для создания TcpClientChannel на клиенте не требуется определять общеизвестный порт. Конструктор по умолчанию для TcpClientChannel выберет доступный порт, который передается серверу во время соединения, чтобы сервер мог послать данные назад клиенту.

Создание нового экземпляра канала немедленно включает сокет на прослушивание состояния, которое можно проверить, вводя в командной строке netstat -а.

Каналы HTTP используются аналогично каналам TCP. Определяется порт, где сервер может создать слушающий сокет. У нас также есть конструктор, в котором можно задать, передавая Boolean, что должен использоваться защищенный протокол HTTP.

Сервер способен слушать несколько каналов. Здесь создаются каналы как HTTP, так и TCP:

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Tcp;

using System.Runtime.Remoting.Channels.Http;

namespace Wrox.ProfessionalCSharp {

/// <summary>

/// Краткое описание Class1.

/// </summary>

public class HelloServer {

public static void Main(string[] args) {

TcpServerChannel tcpChannel = new TcpServerChannel(8086);

HttpServerChannel httpChannel = new HttpServerChannel (8085);

// ...

Класс канала должен реализовать интерфейс IChannel. Интерфейс IChannel имеет два свойства:

□ ChannelIName — только для чтения, которое возвращает имя канала. Имя канала зависит от типа, например, канал HTTP называется HTTP.

□ ChannelPriority — только для чтения с условием, что более одного канала используется для коммуникации между клиентом и сервером. Приоритет определяет порядок каналов. На клиенте канал с более высоким приоритетом выбирается первым для соединения с сервером.

В зависимости от того, является ли канал клиентским каналом или серверным каналом, реализуются дополнительные интерфейсы. Серверные версии каналов реализуют интерфейс IChannelReceiver, клиентские версии — интерфейс IChannelSender.

Классы HttpChannel и TcpChannel используются как для клиентов, так и для серверов. Они реализуют IChannelSender и IChannelReceiver. Эти интерфейсы являются производными из IChannel.

IChannelSender клиентской стороны имеет в дополнение в IChannel единственный метод, называемый CreateMessageSink(), который возвращает объект, реализующий IMessageSink. Интерфейс IMessageSink применяется для размещения синхронных, а также асинхронных сообщений в канале. С помощью интерфейса серверной стороны IChannelReceiver канал можно перевести в режим прослушивания с помощью метода StartListening() и снова остановить с помощью метода StopListening(). Также имеется свойство для доступа к полученным данным.

Информацию о конфигурации обоих каналов получают с помощью свойств классов каналов ChannelName, ChannelPriority и ChannelData. С помощью свойства ChannelData получают информацию об URI, который хранится в классе ChannelDataStore. В классе HttpChannel имеется также свойство Scheme. Ниже представлен вспомогательный метод ShowChannelProperties(), демонстрирующий эти данные.

protected static void ShowChannelProperties(IChannelReceiver channel) {

Console.WriteLine("Name; " + channel.ChannelName");

Console.WriteLine("Priority: " + channel.ChannelPriority);

if (channel is HttpChannel) {

HttpChannel httpChannel = channel as HttpChannel;

Console.WriteLine("Scheme: " + httpChannel.ChannelScheme);

}

ChannelDataStore data = (ChannelDataStore)channel.ChannelData;

foreach (string uri in data.ChannelUris) {

Console.WriteLine("URI: " + uri);

}

Console.WriteLine();

}

После создания каналов вызывается метод ShowChannelProperties():

TcpServerChannel tcpChannel = new TcpServerChannel(8086);

ShowChannelProperties(tcpChannel);

HttpServerChannel httpChannel = new HttpServerChannel(8085);

ShowChannelProperties(httpChannel);

С помощью каналов TCP и HTTP будет получена следующая информация:

Как можно видеть, именем по умолчанию для TcpServerChannel будет tcp, а канал HTTP называется http. Оба канала имеют свойство по умолчанию, равное 1 (в конструкторах заданы порты 8085 и 8086). URI каналов показывает протокол, имя хоста (в данном случае CNagel) и номер порта.

Задание свойств канала

Можно задать все свойства канала в списке с помощью конструктора TcpServerChannel(IDictionary, IServerChannelSinkProvider). Класс ListDictionary реализует IDictionary, поэтому свойства Name, Priority и Port задаются с помощью этого класса.

Для использования класса ListDictionary необходимо объявить использование пространства имен System.Collections.Specialized. В дополнение к параметру IDictionary передается параметр IServerChannelSinkProvider, в данном случае SoapServerFormatterSinkProvider вместо BinaryServerFormatterSinkProvider, который используется по умолчанию для TCPServerChannel. Реализация по умолчанию класса SoapServerFormatterSinkProvider ассоциирует класс SoapServerFormatterSink с каналом, применяющим SoapFormatter для преобразования данных передачи:

ListDictionary properties = new ListDictionary();

properties.Add("Name", "TCP Channel with a SOAP Formatter");

properties.Add("Priority", "20");

properties.Add("Port", "8086");

SoapServerFormatterSinkProvider sinkProvider =

new SoapServerFormatterSinkProvider();

TcpServerChannel tcpChannel =

new TcpServerChannel(properties.sinkProvider);

ShowChannelProperties(tcpChannel);

Вывод, который будет получен из запускаемого на сервере кода, показывает новые свойства канала TCP:

Подключаемость канала

Можно создать специальный канал для отправки сообщений с помощью транспортного протокола, отличного от HTTP или TCP, или расширить существующие каналы:

□ Посылающая часть должна реализовать интерфейс IChannelSender. Наиболее важным является метод CreateMessageSink(), где клиент посылает URL, с помощью него создается экземпляр соединения с сервером. Здесь должен быть создан приемник сообщений, затем он используется прокси для отправки сообщений в канал.

□ Получающая часть должна реализовать интерфейс IChannelReceiver. Необходимо запустить прослушивание с помощью свойства ChannelData, затем ожидать отдельного потока выполнения для получения данных от клиента. После демаршализации сообщения метод ChannelServices.SyncDispatchMessage() может использоваться для отправки сообщения объекту.

Форматтеры

.NET Framework предоставляет два класса форматтера:

□ System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.

□ System.Runtime.Serialization.Formatters.Soap.SoapFormatter.

Форматтер ассоциируется с каналом из-за наличия объектов приемников форматтера и провайдеров источников форматтера.

Оба эти класса форматтера реализуют интерфейс System.Runtime.Remoting.Messaging.IRemotingFormatter, который определяет методы Serialize() и Deserialize() для передачи и приема данных из канала.

Форматтер является подключаемым. При создании класса собственного форматтера экземпляр должен ассоциироваться с используемым каналом. Это делается с помощью приемника форматтера и провайдера приемника форматтера. Как было показано ранее, провайдер источника форматтера, например SoapServerFormatterSinkProvider, может передаваться как аргумент при создании канала. Провайдер источника форматтера реализует для сервера интерфейс IServerChannelSinkProvider, а для клиента IClientChannelSinkProvider. Оба эти интерфейса определяют метод CreateSink(), возвращающий источник форматтера, — SoapServerFormatterSinkProvider даст экземпляр класса SoapServerFormatterSink. На клиентской стороне имеется класс SoapClientFormatterSink, который использует в методах SyncProcessMessage() и AsyncProcessMessage() класс SoapFormatter для сериализации сообщения. SoapServerFormatterSink десериализует сообщение снова с помощью SoapFormatter.

Все эти классы приемника и провайдера способны расширяться и заменяться собственными реализациями.

ChannelServices и RemotingContiguration

Служебный класс ChannelServices используется для регистрации каналов в среде выполнения .NET Remoting. С помощью этого класса можно также получить доступ ко всем зарегистрированным каналам. Это крайне полезно, если для конфигурирования канала используются конфигурационные файлы, так как в этом случае канал создается неявно (см. ниже).

Канал регистрируется с помощью статического метода ChannelServices.RegisterChannel().

Здесь представлен серверный код для регистрации каналов HTTP и TCP:

TcpChannel tcpChannel = new TcpChannel(8086);

HttpChannel httpChannel = new HttpChannel(8085);

Channel Services.RegisterChannel(tcpChannel);

СhannelServices.RegisterChannel(httpChannel);

Служебный класс ChannelServices можно теперь использовать для отправки синхронных и асинхронных сообщений и для отмены регистрации определенных каналов. Свойство RegisteredChannels возвращает массив IChannel всех зарегистрированных каналов. Возможно также и< пользование метода GetChannel() для доступа к определенному каналу по его имени. С помощью ChannelServices пишется специальная административная утилита для управления каналами. Вот небольшой пример показывающий как можно остановить режим прослушивания канала:

HttpServerChannel channel =

(HttpServerChannel)ChannelServices.GetChannel("http");

channel.StorListening(null);

Класс RemotingConfiguration — другой служебный класс .NET Remoting. На серверной стороне он используется для регистрации типов удаленных объектов активированных на сервере объектов и для маршализации удаленных объектов в ссылочный класс маршализованного объекта ObjRef. ObjRef является сериализуемым представлением объекта, которое было послано по линии связи. На клиентской стороне работает RemotingServices, демаршализуя удаленный объект, чтобы создать прокси из объектной ссылки.

Вот серверный код для регистрации хорошо известного типа удаленного объекта в RemotingServices:

RemotingConfiguration.RegisterWellKnownServiceType(

typeof(Hello),                   // Тип

"Hi",                            // URI

WellKnownObjectMode.SingleCall); // Режим

Первый аргумент метода RegisterWellKnownServiceType() — Wrox.ProfessionalCSharp.Hello определяет тип удаленного объекта. Второй аргумент — Hi является универсальным идентификатором ресурса удаленного объекта, который используется клиентом для доступа к удаленному объекту. Последний аргумент — режим удаленного объекта. Режим может быть значением перечисления WellKhownObjectMode: SingleCall или Singleton.

□ SingleCall означает, что объект не сохраняет состояние. Для каждого вызова удаленного объекта создается новый экземпляр. Объект SingleCall создается на сервере с помощью метода RemotingConfiguration.RegisterWellKnownServiceТуре() и аргумента WellKnownObjectMode.SingleCall. Это очень эффективно для сервера, так как получается, что не требуется поддерживать ресурсы может быть для тысяч клиентов.

□ С помощью режима Singleton объект совместно используется всеми клиентами сервера. Такие объектные типы могут применяться, если желательно предоставить доступ к некоторым данным всем клиентам. Это не должно быть проблемой для читаемых данных, но для данных чтения-записи необходимо знать о вопросах блокировки и масштабируемости. Объект Singleton создается на сервере с помощью метода RemotingConfiguration.RegisterWellKnownServiceType() и аргумента WellKnownObjectMode.Singleton. Необходимо убедиться, что данные не могут быть повреждены, когда клиенты получают доступ одновременно, но нужно и проверять, что блокирование сделано достаточно эффективно для достижения требуемой масштабируемости.

Сервер для активизированных клиентом объектов

Если удаленный объект должен хранить состояние для определенного клиента, то можно использовать активизированные клиентом объекты. В следующем разделе будут рассмотрены возможности клиентской стороны. В частности, как вызывать объекты, активизированные сервером, и объекты, активизированные клиентом. На серверной стороне активизированные клиентом объекты должны регистрироваться по-другому, чем объекты, активизированные сервером.

Вместо вызова RemotingConfiguration.RegisterWellKnownType() необходимо вызвать RemotingServices.RegisterActivatedServiceType(). С помощью этого метода определяются только типы, но не URI. Причина этого заключается в том, что для активированных клиентом объектов создаются экземпляры различных объектных типов с помощью одного URI. URI для всех активированных клиентом объектов должен быть определен с помощью RemotingConfiguration.ApplicationName:

RemotingConfiguration.ApplicationName = "HelloServer";

RemotingConfiguration.RegisterActivatedServiceType(typeof (Hello));

Активизация объектов

Для клиентов возможно использование и создание удаленных объектов с помощью класса Activator. Мы можем получить прокси для активированного сервером или хорошо известного удаленного объекта с помощью метода GetObject(). Метод CreateInstance() возвращает прокси для активированного клиентом удаленного объекта.

Вместо класса Activator для активации удаленных объектов используется также оператор new. Чтобы сделать это, удаленный объект должен конфигурироваться внутри клиента с помощью класса RemotingConfiguration.

URL-приложения

Во всех сценариях активации необходимо определять URL удаленного объекта. Этот URL является тем же самым, что и в браузере Web. Первая часть определяет протокол, за которым следует имя сервера или адрес IP, номер порта и URI, определенный при регистрации удаленного объекта на сервере в таком виде:

protocol://server:port/URI

Мы все время используем в нашем коде два примера URL: определяем протокол http и tcp, имя сервера localhost, номер порта 8085 и 8086, и URI как Hi, что дает нам запись:

http://localhost:8085/Hi

tcp://localhost:8086/Hi

Активация хорошо известных объектов

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Tcp;

/// ...

TcpClientChannel channel = new TcpClientChannel();

ChannelServices.RegisterChannel(channel);

Hello obj = (Hello)Activator.GetObject(

typeof(Hello), "tcp://localhost:8086/Hi");

GetObject() является статическим методом класса System.Activator, который вызывает метод RemotingServices.Connect() для возврата объекта прокси удаленному объекту. Первый аргумент определяет тип удаленного объекта. Прокси реализует все открытые и защищенные методы и свойства, так что клиент может вызывать эти методы так же, как для реального объекта. Второй аргумент является URL удаленного объекта. Здесь используется строка tcp://localhost:8086/Hello, где tcp — протокол, localhost:8086 — имя хоста и номер порта и, наконец, Hello — это URI объекта, который был определен с помощью RemotingConfiguration.RegisterWellKnownServiceType().

Вместо Activator.GetObject() можно также использовать RemotingServices.Connect():

Hello obj =

(Hello)RemotingServices.Connect(typeof(Hello), "tcp://localhost:8086/Hi");

Если вы предпочитаете задать просто оператор new для активизации хорошо известных удаленных объектов, то удаленный объект можно зарегистрировать на клиенте с помощью все того же RemotingConfiguration.RegisterWellKnownClientType(). Здесь понадобятся похожие аргументы: тип удаленного объекта и URI. Теперь можно использовать оператор new, который на самом деле не создает новый удаленный объект, а возвращает прокси аналогично Activator.GetObject(). Если удаленный объект регистрируется с флажком WellKnownObjectMode.SingleCall, правило остается тем же самым: удаленный объект создается с каждым вызовом метода:

RemotingConfiguration.RegisterWellKnownClientType(

typeof(Hello), "tcp://localhost:8086/Hi");

Hello obj = new Hello();

Активизация объектов, активизированных клиентом

Удаленные объекты могут хранить состояние для клиента. Activator.CreateInstance() создает активированный клиентом удаленный объект. С помощью метода Activator.GetObject() удаленный объект создается при вызове метода и разрушается, когда метод заканчивается. Объект не хранит состояние сервера. В этом отличие от Activator.CreateInstance(). С помощью статического метода CreateInstance() запускается последовательность активации для создания удаленного объекта. Этот объект живет, пока не закончится время аренды и не произойдет сборка мусора. Позже мы рассмотрим механизм аренды.

Некоторые из перезагруженных методов Activator.CreateInstance() используются только для создания локальных объектов. Для получения удаленных объектов требуется метод, куда можно передавать activationAttributes. Один из таких перезагруженных методов используется в примере. Этот метод получает два строковых параметра, первый из которых является именем сборки, второй — типом, а третий — массивом объектов. В объектном массиве канал и имя объекта определяются с помощью UrlAttribute. Чтобы использовать класс UrlAttribute, должно быть определено пространство имен System.Runtime.Remoting.Activation.

object [] attrs = { new UrlAttribute("tcp://localhost:8086/Hello") };

ObjectHandle handle =

Activator.CreateInstance("RemoteHello",

"Wrox.ProfessionalCSharp.Hello", attrs);

if (handle == null) {

Console.WriteLine("could not locate server");

return 0;

}

Hello obj = (Hello)handle.Unwrap();

Console.WriteLine(obj.Greeting("Christian"));

Конечно, для активизированных клиентом объектов также возможно использование оператора new вместо класса Activator. Таким образом, мы должны зарегистрировать активизированный клиентом объект с помощью RemotingConfiguration.RegisterActivatedClientType(). В архитектуре активизированных клиентом объектов оператор new не только возвращает прокси, но также создает удаленный объект:

RemotingConfiguration.RegisterActivatedClientType(

typeof (Hello), "tcp://localhost:8086/HelloServer");

Hello obj = new Hello();

Объекты прокси

Методы Activator.GetObject() и Activator.CreateInstance() возвращают клиенту прокси. На самом деле создается два прокси (прозрачный и реальный). Прозрачный прокси выглядит как удаленный объект, он реализует все открытые методы удаленного объекта, вызывая метод Invoke() реального прокси. Реальный прокси посылает сообщения в канал посредством приемников сообщений.

С помощью RemotingServices.IsTransparentProxy() проверяется, является ли объект на самом деле прозрачным прокси. Можно также добраться до реального прокси с помощью RemotingServices.GetRealProxy(). Используя отладчик, легко получить все свойства реального прокси:

ChannelServices.RegisterChannel(new TCPChannel());

Hello obj =

(Hello)Activator.GetObject(typeof(Hello), "tcp://localhost:8086/Hi");

if (obj == null) {

Console.WriteLine("could not locate server");

return 0;

}

if (RemotingServices.IsTransparentProxy(Obj)) {

Console.WriteLine("Using a transparent proxy");

RealProxy proxy = RemotingServices.GetRealProxy(obj);

// proxy.Invoke(message);

}

Подключаемость прокси

Реальный прокси может заменяться специально созданным прокси. Специально созданный прокси расширяет базовый класс System.Runtime.Remoting.RealProxy. Мы получаем тип удаленного объекта в конструкторе специального прокси. Вызов конструктора для RealProxy создает прозрачный прокси в дополнение к реальному. В конструкторе могут быть доступны зарегистрированные каналы с помощью класса ChannelServices для создания приемника сообщений в методе IChannelSender.CreateMessageSink(). Помимо реализации конструктора, специальный канал переопределяет метод Invoke(). В Invoke() получают сообщение, которое затем анализируется и посылается в приемник сообщений.

Сообщения

Прокси посылает сообщение в канал. На серверной стороне будет сделан вызов метода после анализа сообщения, поэтому давайте рассмотрим сообщения.

Имеется несколько классов сообщений для вызова методов, ответов, возврата сообщений и т.д. Все классы сообщений должны реализовывать интерфейс IMessage. Этот интерфейс имеет единственное свойство: Properties. Это свойство представляет словарь, где URI указывает объект, а вызываемые MethodName, MethodSignature, TypeName, Args и CallContext являются пакетами.

Ниже представлена иерархия классов и интерфейсов сообщений:

Посылаемое реальному прокси сообщение является MethodCall. С помощью интерфейсов IMethodCallMessage и IMethodMessage мы имеем более простой доступ к свойствам сообщения, чем через интерфейс IMessage. Вместо использования интерфейса IDictionary мы имеем прямой доступ к имени метода, URI, аргументам и т.д. Реальный прокси возвращает ReturnMessage прозрачному прокси.

Приемники сообщений

Метод Activator.GetObject() вызывает RemotingServicesConnect() для соединения с хорошо известным объектом. В методе Connect() происходит Unmarshal(), где создается не только прокси, но и уполномоченные приемники. Прокси использует цепочку уполномоченных приемников для передачи сообщения в канал. Все приемники являются перехватчиками, которые могут изменять сообщение и выполнять некоторые дополнительные действия, такие как создание блокировки, запись события, выполнение проверки безопасности и т.д.

Все приемники событий реализуют интерфейс IMessageSink. Такой интерфейс определяет одно свойство и два метода:

□ Свойство NextSink используется приемником для получения следующего приемника и передачи сообщения дальше.

□ Для синхронных сообщений вызывается метод SyncProcessMessage() предыдущим приемником или удаленной инфраструктурой. Он имеет параметр IMessage для отправки и возврата сообщения.

□ Для асинхронных сообщений вызывается метод AsyncProcessMessage() предыдущим приемником в цепочке или удаленной инфраструктурой. AsyncProcessMessage() имеет два параметра, куда могут передаваться сообщение и приемник сообщения, который получает ответ.

Рассмотрим три доступных для использования приемника сообщения.

Уполномоченный приемник

Можно получить цепочку уполномоченных приемников с помощью интерфейса IEnvoyInfo. Маршализованная объектная ссылка ObjRef имеет свойство EnvoyInfo, которое возвращает интерфейс IEnvoyInfo. Список уполномоченных приемников создается из серверного контекста, поэтому сервер может добавлять функциональность клиенту. Уполномоченные приемники собирают информацию об идентичности клиента и предают ее серверу.

Приемник серверного контекста

Когда сообщение получено на серверной стороне канала, оно передается приемникам серверного контекста. Последний из этих приемников направляет сообщение в цепочку объектных приемников.

Объектный приемник

Объектный приемник ассоциируется с определенным объектом. Если объектный класс определяет атрибуты определенного контекста, то для объекта создаются приемники контекста.

Передача объектов в удаленные методы

Типы параметров для вызовов удаленных методов не ограничены только базовыми типами данных, но могут также быть классами, которые определяет программист. Для удаленных методов различают три типа классов:

□ Классы, маршализуемые по значению, обычно сериализуются через канал. Классы, которые должны быть маршализованы, либо реализуют интерфейс ISerializable, либо помечаются с помощью атрибута [Serializable]. Объекты этих классов не имеют удаленной идентичности, так как весь объект маршализуется через канал, а объект, который сериализуется клиенту, является независимым от серверного объекта (или наоборот). Классы, маршализуемые по значению, называются также несвязанными классами, так как они не имеют данных, которые зависят от домена приложения.

□ Классы, маршализуемые по ссылке, имеют удаленную идентичность. Объекты не передаются по линиям связи, а вместо этого возвращается прокси. Класс, который маршализуется по ссылке, должен выводиться из MarshalByRefObject. Объекты MarshalByRefObject называют объектами, связанными с доменом приложения. Специализированной версией MarshalByRefObject является класс ContextBoundObject: абстрактный класс ContextBoundObject выводится из MarshalByRefObject. Если класс выводится из ContextBoundObject, требуется прокси даже в том же самом домене приложения, когда пересекаются границы контекстов.

□ Классы, которые не являются сериализуемыми и не выводятся из MarshalByRefObject, не могут использоваться в параметрах открытых методов удаленных объектов. Эти классы связаны с доменом приложения, где они созданы. Такие классы должны использоваться, если класс имеет члены данных, допустимые только в домене приложения, такие как дескриптор файла Win32.

Чтобы увидеть маршализацию в действии, изменим удаленный объект для пересылки двух объектов клиенту: пусть класс MySerialized посылает маршализацию по значению, а класс MyRemote маршализует по ссылке. В методах сообщение записывается на консоль, чтобы можно было проверять, сделан ли вызов на клиенте или на сервере. Кроме того, класс Hello изменяется, чтобы возвращать экземпляры MySerilized и MyRemote:

using System;

namespace Wrox.ProfessionalCSharp {

[Serilizable]

public сlass MySerilized {

public MySerilized(int val) {

a = val;

}

public void Foo() {

Console.WriteLine("MySerialized.Foo called");

}

public int A {

get {

Console.WriteLine("MySerialized A called");

return a;

}

set {

a = value;

}

}

protected int a;

}

public class MyRemote : System.MarshalByRefObject {

public MyRemote(int val) {

a = val;

}

public void Foo() {

Console.WriteLine("MyRemote.Foo called");

}

public int A {

get

Сonsole.WriteLine("MyRemote.A called");

return a;

}

set {

a = value;

}

}

protected int a;

}

/// <summary>

/// Краткое описание Class1

/// </summary>

public class Hello : System.MarshalByRefObject {

public Hello() {

Console.WriteLine("Constructor called");

}

~Hello() {

Console.WriteLine("Destructor called");

}

public string Greeting(string name) {

Console.WriteLine("Greeting called");

return "Hello, " + name;

}

public MySerialized GetMySerilized() {

return new MySerialized(4711);

}

public MyRemote GetMyRemote() {

return new MyRemote(4712);

}

}

}

Клиентское приложение также необходимо изменить, чтобы увидеть результаты при использовании маршализации объектов по значению и по ссылке. Мы вызываем методы GetMySerialized() и GetMyRemote(), чтобы получить новые объекты и проверить, не используется ли прозрачный прокси.

ChannelServices.RegisterChannel(new TcpChannel());

Hello obj =

(Hello)Activator.GetObject(typeof(Hello),

"tcp://localhost:8086/Hi");

if (obj == null) {

Console.WriteLine("could not locate server");

return;

}

MySerialized ser = obj.GetMySerialized();

if (!RemotingServices.IsTransparentProxy(ser)) {

Console.WriteLine("ser is not a transparent proxy");

}

ser.Foo();

MyRemote rem = obj.GetMyRemote();

if (RemotingServices.IsTransparentProxy(rem)) {

Console.WriteLine("rem is a transparent proxy");

}

rem.Foo();

В консольном окне клиента видно, что объект ser вызывается на клиенте. Этот объект не является прозрачным прокси, так как он сериализуется клиенту. В противоположность этому, объект rem на клиенте является прозрачным прокси. Методы, вызванные для этого объекта, передаются на сервер:

В серверном выводе можно видеть, что метод Foo() вызывается с удаленным объектом MyRemote:

Направляющие атрибуты

Удаленные объекты никогда не передаются по линиям связи в отличие от типов данных значений и сериализуемых классов. Иногда желательно послать данные только в одном направлении. Это особенно важно, когда данные передаются по сета. С помощью COM можно было объявить для аргументов направляющие атрибуты [in], [out] и [in, out], если данные должны посылаться на сервер, клиенту или в обоих направлениях.

В C# существуют аналогичные атрибуты как часть языка: параметры методов ref и out. Параметры методов ref и out могут использоваться для типов данных значений и для ссылочных типов, которые способны сериализоваться. С помощью параметра ref аргумент маршализуется в обоих направлениях, out идет от сервера клиенту, а в отсутствие параметра метода посылает данные серверу.

Управление временем жизни

Как клиент и сервер определяют, какая возникла проблема и что при этом другая сторона более недоступна?

Для клиента ответ может быть коротким. Как только клиент вызывает метод для удаленного объекта, мы получаем исключение типа System.Runtime.Remoting.RemotingException. Необходимо просто обработать это исключение и сделать, например, повторную попытку или записать в журнал, информировать пользователя и т.д.

А что же сервер? Когда сервер обнаруживает, что клиент отсутствует (что означает возможность очистить ресурсы, которые он удерживает для клиента)? Если ждать следующего вызова метода с клиента, он может никогда не появиться. В области COM протокол DCOM использовал механизм ping. Клиент посылал на сервер ping с информацией об используемых объектах. Так как клиент мог иметь на сервере сотни используемых клиентов, то, чтобы сделать этот механизм более эффективным, посылалась информация не обо всех объектах, а только о различии с предыдущим ping.

Этот механизм был эффективен в LAN, но не подходит для Интернета. Подумайте о тысячах или миллионах клиентов, посылающих ping-информацию на сервер. .NET Remoting использует существенно лучшее масштабируемое решение для управления временем жизни — LDGC (Leasing Distributed Garbage Collector — Сборщик мусора распределенной аренды.

Это управление временем жизни активно только для активизированных клиентом объектов. Объекты SingleCall могут разрушаться после каждого вызова метода, так как они не сохраняют состояние. Активизированные клиентам объекты имеют состояние и нам необходимо знать о ресурсах. Для активизированных клиентом объектов, на которые ссылаются из вне домена приложения, создается аренда. Аренда имеет время аренды. Когда время аренды достигает нуля, аренда заканчивается, удаленный объект отсоединяется и, наконец, мусор убирается.

Для управления временем жизни можно сконфигурировать следующие значения:

□ LeaseTime определяет время, пока не закончится аренда.

□ RenewOnCallTime является временем, которое аренда задает для вызова метода, если текущее время аренды имеет меньшее значение.

□ Если спонсор недоступен в течение SponsorshipTimeout, то удаленная инфраструктура ищет следующего спонсора. Если больше нет спонсоров, аренда заканчивается.

□ LeaseManagerPollTime определяет интервал времени, в течение которого менеджер аренды проверяет отслуживший объект.

Конфигурация аренды

Значение по умолчанию (секунды)

LeaseTime

300

RenewOnCallTime

120

SponsorshipTimeout

120

LeaseManagerPollTime

10

Обновление аренды

Как показано в таблице, время аренды по умолчанию для объекта составляет 300 секунд. Если клиент вызывает метод на объекте, когда аренда истекла, возникает исключение. Если имеется клиент, где удаленный объект может понадобиться на время более 300 с, то существует три способа обновления аренды:

□ Неявное обновление делается автоматически, когда клиент вызывает метод на удаленном объекте. Если текущее время аренды меньше, чем значение RenewOnCallTime, то аренда задается как RenewOnCallTime.

□ При явном обновлении клиент определяет новое время аренды. Это делается с помощью метода Renew() из интерфейса ILease. Доступ к интерфейсу ILease можно получить, вызывая метод GetLifetimeService() на прозрачном прокси.

□ Третьей возможностью обновления аренды является спонсорство. Клиент может создать спонсора, который реализует интерфейс ISponsor и регистрирует спонсора в службах аренды с помощью метода Register() из интерфейса ILease. Когда аренда заканчивается, у спонсора запрашивают ее продления. Механизм спонсорства используется, если на сервере требуются долгоживущие удаленные объекты.

Классы, используемые для управления временем жизни

ClientSponsor является спонсором, который реализует интерфейс ISponsor. Он применяется на клиентской стороне для продления аренды. С помощью интерфейса ILease можно получить всю информацию об аренде, все свойства аренды, а также время и состояние текущей аренды. Состояние определяется с помощью перечисления LeaseState. С помощью служебного класса LifetimeServices можно получить и задать свойства аренды для всех удаленных объектов в домене приложения.

Пример: получение информации об аренде

В этом небольшом примере кода доступ к информации аренды осуществляется с помощью вызова метода GetLifetimeService() на прозрачном прокси. Для интерфейса ILease необходимо открыть пространство имен System.Runtime.Remoting.Lifetime:

Помните, что эти действия годятся только для активизированных клиентом объектов. Экземпляры объектов SingleCall создаются для каждого вызова метода, поэтому механизм аренды не используется.

ILease lease = (ILease)obj.GetLifetimeService();

if (lease != null) {

Console.WriteLine("Lease Configuration:");

Console.WriteLine(

"InitialLeaseTime: " + lease.InitialLeaseTime);

Console.WriteLine(

"RenewOnCallTime: " + lease.RenewOnCallTime);

Console.WriteLine(

"SponsorshipTimeout: " + lease.SponsorshipTimeout);

Console.WriteLine(lease.CurrentLeaseTime);

}

В результате получается следующий вывод в окне клиентской консоли:

Изменение используемых по умолчанию конфигураций аренды

Сам сервер может изменить используемую по умолчанию конфигурацию аренды для всех удаленных объектов, используя служебный класс System.Runtime.Remoting.Lifetime.LifetimeServices:

LifetimeServices.LeaseTime = TimeSpan.FromMinutes(10);

LifetimeServices.RenewOnCallTime = TimeSpan.FromMinutes(2);

Если требуются другие используемые по умолчанию значения параметров времени жизни, в зависимости от типа удаленного объекта, можно изменить конфигурацию аренды удаленного объекта, переопределяя метод InitializeLifetimeService() базового класса MarshalByRefObject:

public class Hello : System.MarshalByRefObject {

public Hello() {

Console.WriteLine("Constructor called");

}

~Hello() {

Console.WriteLine("Destructor called");

}

public override Object InitializeLifetimeService() {

ILease lease = (ILease)base.InitializeLifetimeService();

lеase.InitialLeaseTime = TimeSpan.FromMinutes(10);

lease.RenewOnCallTime = TimeSpan.FromSeconds(40);

return lease;

}

}

Конфигурация служб времени жизни также задается с помощью конфигурационного файла.

Конфигурационные файлы

Вместо записи конфигурации канала и объекта в исходном коде, можно использовать конфигурационные файлы. Таким способом реконфигурируют канал, добавляют дополнительные каналы и т.д., не изменяя исходный код. Для этого, как и для всех других конфигурационных файлов на платформе .NET. используется XML на основе тех же самых приложений, о которых было написано в главе 10. В те же самые файлы в главе 25 будет добавлена конфигурация системы безопасности. В .NET Remoting имеются атрибуты и элементы XML для конфигурирования канала и удаленных объектов. Файл должен иметь то же самое имя, что и исполнимый файл, за которым следует .config. Для сервера HelloServer.exe конфигурационным файлом будет HelloServer.exe.config. В коде, загружаемом с web-сайта издательства Wrox, можно найти примеры конфигурационных файлов в корневом каталоге примеров с именами clientactivated.config, wellknown.config и wellknownhttp.config. Чтобы воспользоваться ими, переименуйте их, как показано выше, и поместите в каталог, содержащий исполнимый файл.

Вот только один пример, как мог бы выглядеть такой файл. Мы рассмотрим все различные конфигурационные параметры:

<configuration>

<system.runtime.remoting>

<application name="Hello">

<service>

<wellknown mode="SingleCall" type="Wrox.ProfessionalCSharp.Hello, RemoteHello" objectUri="Hi" />

</service>

<channels>

<channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" port="6791" />

<channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" port="6792" />

</channels>

</application>

</system.runtime.remoting>

</configuration>

<configuration> является корневым элементом XML для всех конфигурационных файлов .NET. Все удаленные конфигурации можно найти в подэлементе <system.runtime.remoting>. <application> является подэлементом <system.runtime.remoting>.

Посмотрим на основные элементы и атрибуты в <system.runtime.remoting>:

□ В элементе <application> определяется имя приложения с помощью атрибута name. На серверной стороне это имя сервера, а на клиентской стороне — имя клиентского приложения. Пример серверной конфигурации <application name="Hellо"> определяет имя удаленного приложения Hello, которое используется клиентом как часть URL для доступа к удаленному объекту.

□ На сервере элемент <service> используется для определения совокупности удаленных объектов. Он может иметь подэлементы <wellknown> и <activated> вместе с определенным типом удаленного объекта — well known или client-activated.

□ Клиентской частью элемента <service> является <client>. Подобно элементу <service> он может иметь подэлементы <wellknown> и <activated> для определения типа удаленного объекта. В отличие от <service> элемент <client> имеет атрибут url для определения URL удаленного объекта.

□ <wellknown> является элементом, который используется на сервере и на клиенте для определения хорошо известных удаленных объектов. Серверная часть выглядит так:

<wellknown mode="SingleCall" type="Wrox.ProfessionalCSharp.Hello, RemoteHello" objectURI="Hi" />

□ В то время как атрибут mode может принимать значения SingleCall или Singleton, type является типом удаленного класса, включая пространство имен Wrox.ProfessionalCSharp.Hello, за которым следует имя сборки RemoteHello. Именем удаленного объекта является objectURI, который зарегистрирован в канале. На клиенте атрибут type является таким же, как и для серверной версии. mode и objectURI не нужны, вместо них используется атрибут url для определения пути доступа к удаленному объекту: протокол, имя хоста, номер порта, имя приложения и URI объекта:

<wellknown type="Wrox.ProfessionalCSharp.Hello, RemoteHello" url="tcp://localhost:6791/Hello/Hi" />

□ Элемент <activated> используется для активированных клиентом объектов. С помощью атрибута type должны быть определены тип данных и сборка как для клиентского, так и для серверного приложений:

<activated type="Wrox.ProfessionalCSharp.Hello, RemoteHello" />

□ Для определения канала, используется элемент <channel>. Это подэлемент <channels>, так что совокупность каналов можно сконфигурировать для одного приложения. Его использование аналогично для клиентов и серверов. Атрибут type используется для определения типа канала и сборки. Атрибут port является номером порта, который нужен только для серверной конфигурации:

<channels>

<channel type = "System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" port="6791" />

<channel type = "System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" port="6792" />

</channels>

Конфигурация сервера для хорошо известных объектов

Этот пример файла wellknown.config имеет значение Hello для свойства Name. Мы используем канал TCP для прослушивания порта 6791, а канал HTTP для прослушивания порта 6792. Класс удаленного объекта —Wrox.ProfessionalCSharp.Hello в сборке RemoteHello.dll, объект в канале называется Hi, и используется режим SingleCall:

<configuration>

<system.runtime.remoting>

<application name="Hello">

<service>

<wellknown mode="SingleCall" type="Wrox.ProfessionalCSharp.Hello, RemoteHello" objectUri ="Hi" />

</service>

<channels>

<channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" port="6791" />

<channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" port="6792" />

</channels>

</application>

</system.runtime.remoting>

</configuration>

Конфигурация клиента для хорошо известных объектов

Для хорошо известных объектов в клиентском конфигурационном файле wellknown.config необходимо определить сборку и канал. Типы для удаленного объекта можно найти в сборке RemoteHello.dll, Hi является именем объекта в канале, a URI для удаленного типа Wrox.ProfessionalCSharp.Hello — это tcp://localhost:6791/Hi. На клиенте также работает канал TCP, но на клиенте не определяется порт, поэтому выбирается свободный порт.

<configuration>

<system.runtime.remoting>

<application name="Client">

<client url="tcp:/localhost:6791/Hello">

<wellknown type = "Wrox.ProfessionalCSharp.Hello, RemoteHello" url="tcp://localhost:6791/Hello/Hi" />

</client>

<channels>

<channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" />

</channels>

</application>

</system.runtime.remoting>

</configuration>

Внесем небольшое изменение в конфигурационный файл и можем использовать канал HTTP (как видно в wellknownhttp.config):

<client url="http://localhost:6792/Hello">

<wellknown type="Wrox.ProfessionalCSharp.Hello, RemoteHello" url="http://localhost:6792/Hello/Hi" />

</client>

<channels>

<channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" />

</channels>

Серверная конфигурация для активизированных клиентом объектов

Преобразуя только конфигурационный файл (который находится в clientactivated.config), можно изменить сервер с активизированных сервером объектов на активизированные клиентом объекты. Здесь определяется подэлемент <activated> элемента <service>. С его помощью для серверной конфигурации должен быть определен атрибут type. Атрибут name элемента application определяет URI:

<configuration>

<system.runtime.remoting>

<application name="HelloServer">

<service>

<activated type="Wrox.ProfessionalCSharp.Hello, RemoteHello" />

</service>

<channels>

<channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" ports="6788" />

<channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" ports="6789" /»

</channels>

</application>

</system.runtime.remoting>

</configuration>

Клиентская конфигурация для активизированных клиентом объектов

Файл clientactivated.config определяет активированный клиентом удаленный объект с помощью атрибута url элемента <client> и атрибута type элемента <activated>:

<configuration>

<system.runtime.remoting>

<application>

<client url="http://localhost:6788/HelloServer" >

<activated type="Wrox.ProfessionalCSharp.Hello, RemoteHello" />

</client>

<channels>

<channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" />

<channel type="System.Runtime.Remoting.Channels.Tcp.TcpChannel, System.Runtime.Remoting" />

</channels>

</application>

</system.runtime.remoting>

</configuration>

Серверный код, использующий конфигурационные файлы

В серверном коде необходимо сконфигурировать удаленное использование статического метода Configure() из класса RemotingConfiguration. Здесь создаются экземпляры всех определяемых каналов. Может быть мы захотим также узнать о конфигурациях каналов из серверного приложения. Поэтому созданы статические методы ShowActivatedServiceTypes() и ShowWellKnovmServiceTypes(), которые вызываются после загрузки и запуска удаленной конфигурации:

public static void Main(string[] args) {

RemotingConfiguration.Configure("HelloServer.exe.config");

Console.WriteLine(

"Application: " + RemotingConfiguration.ApplicationName);

ShowActivatedServiceTypes();

ShowWellKnownServiceTypes();

System.Console.WriteLine("hit to exit");

System.Console.ReadLine();

return;

}

Эти две функции показывают данные конфигурации хорошо известных и активированных клиентом типов:

public static void ShowWellKnownServiceTypes() {

WellKnownServiceTypeEntry[] entries =

RemotingConfiguration.GetRegisteredWellKnownServiceTypes();

foreach (WellKnownServiceTypeEntry entry in entries) {

Console.WriteLine("Assembly: " + entry.AssemblyName);

Console.WriteLine("Mode: " + entry.Mode);

Console.WriteLine("URI " + entry.ObjectUri);

Console.WriteLine("Type: " + entry.TypeName);

}

}

public static void ShowActivatedServiceTypes() {

ActivatedServiceTypeEntry[] entries =

RemotingConfiguration.GetRegisteredActivatedServiceTypes();

foreach(ActivatedServiceTypeEntry entry in entries) {

Console.WriteLine("Assembly: " + entry.AssemblyName);

Console.WriteLine("Type: " + entry.TypeName);

}

}

Клиентский код, использующий конфигурационные файлы

В клиентском коде с помощью конфигурационного файла client.exe.config нужно сконфигурировать только удаленные службы. После этого можно использовать оператор new для создания новых экземпляров класса Remote независимо от того, происходит ли работа с активированными сервером или с активированными клиентов удаленными объектами. Но помните, что существует небольшая разница. Для активированных клиентом объектов теперь можно использовать произвольные конструкторы с помощью оператора new. Это невозможно для активированных сервером объектов и не имеет смысла в этом случае: объекты SingleCall не могут иметь состояния, так как они разрушаются вместе с каждым вызовом, объекты Singleton создаются только однажды. Вызов произвольных конструкторов полезен только для активированных клиентом объектов, так как только для этого вида объектов оператор new реально вызывает конструктор удаленного объекта:

RemotingConfiguration.Configure("HelloClient.exe.config");

Hello obj = new Hello();

if (obj == null) {

Console.WriteLine("could not locate server");

return 0;

}

for (int i=0; i < 5; i++) {

Console.WriteLine(obj.Greeting("Christian"));

}

Службы времени жизни в конфигурационных файлах

Аренда конфигурации для удаленных серверов также может делаться с помощью конфигурационных файлов приложений. Элемент <lifetime> имеет атрибуты leaseTime, sponsorshipTimeOut, renewOnCallTime и pollTime:

<configuration>

<system.runtime.remoting>

<application>

<lifetime leaseTime="15M" sponsorshipTimeOut="4M" renewOnCallTime="3M" pollTime="30s" />

</application>

</system.runtime.remoting>

</configuration>

Используя конфигурационные файлы, можно изменить удаленную конфигурацию, редактируя файлы вместо работы с исходным кодом. Легко изменить канал для использования HTTP вместо TCP, изменить порт, имя канала, и т. д. С помощью добавления одной строки сервер может слушать два канала вместо одного.

Инструменты для файлов удаленной конфигурации

Не обязательно начинать создавать конфигурационный файл XML для .NET Remoting с чистого листа. Для этого существует несколько инструментов:

□ При использовании версии .NET Remoting Beta 1 можно найти пример convertconfig.exe в списке примеров Framework SDK. С помощью этого инструмента можно преобразовать использовавшийся ранее компактный формат файлов в новый формат на основе XML.

□ С помощью примера configfilegen.exe можно создать конфигурационный файл из сборки. Запустите эту программу без параметров, чтобы увидеть все возможные конфигурации. Следующая командная строка создает активированный клиентом (-а) конфигурационный файл для сервера (-s).

configfilegen -ia:RemoteHello.dll -ос:HelloServer.exe.config -s -a

Системный администратор использует утилиту .NET Admin, чтобы реконфигурировать существующие конфигурационные файлы. Утилиту .NET Admin можно запустить с помощью команды:

mmc mecorcfg.msc

При помощи этой утилиты можно изменить значения времени жизни, URI удаленных объектов и свойства канала.

Приложения хостинга

До этого момента все примеры серверов выполнялись на автономных (self-hosted) серверах .NET. Автономный сервер должен запускаться вручную. Удаленный сервер .NET может запускаться во множестве других типов приложений. В службе Windows сервер автоматически запускается во время старта, и кроме того, процесс может выполняться с полномочиями системной учетной записи. Создание служб Windows описано в главе 24.

Хостинг удаленных серверов в ASP.NET

B ASP.NET существует специальная поддержка для серверов .NET Remoting. ASP.NET может использоваться для автоматического запуска удаленных серверов. В противоположность приложениям exe, ASP.NET Remoting использует для конфигурации другой файл.

Для того чтобы использовать инфраструктуру Информационного сервера Интернета (IIS) и ASP.NET, необходимо создать класс, произвольный из System.MarshalByRefObject, который имеет конструктор по умолчанию. Использованный ранее код для нашего сервера с целью создания и регистрации канала больше не требуется; это делается средой выполнения ASP.NET. Необходимо только создать виртуальный каталог на сервере Web, который отображает каталог, куда помещается конфигурационный файл web.config. Сборка удаленного класса должна находиться в подкаталоге bin.

Чтобы сконфигурировать виртуальный каталог на сервере Web, воспользуйтесь Информационными службами ММС. Выберите Default Web Site и, открыв меню Action, создайте новый виртуальный каталог.

Конфигурационный файл web.config на сервере Web должен быть помещен в домашний каталог виртуального сайта Web. Согласно используемой по умолчанию конфигурации IIS, используемый канал слушает порт 80.

<configuration>

<system.runtime.remoting>

<application>

<service>

<wellknown mode="SingleCall" type="Wrox.ProfessionalCSharp.Hello, RemoteHello" objectUri = "HelloService.soap" />

</service>

</application>

</system.runtime.remoting>

</configuration>

Клиент может теперь соединиться с удаленным объектом, используя следующий конфигурационный файл. URL, который должен быть определен здесь для удаленного объекта, является локальным хостом сервера Web, за ним следует имя приложения Web RemoteHello, которое было определено при создании виртуального сайта Web и URI удаленного объекта HelloService.soap, определенного в файле web.config. Не обязательно определять порт номер 80, так как это порт по умолчанию для протокола http:

<configuration>

<system.runtime.remoting>

<application>

<client url="http:/localhost/RemoteHello">

<wellknown type="Wrox.ProfessionalCSharp.Hello, RemoteHello" url="http://localhost/RemoteHello/HelloService.soap" />

</client>

<channels>

<channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" />

</channels>

</application>

</system.runtime.remoting>

</configuration>

Хостинг удаленных объектов в ASF.NET поддерживает только хорошо известные объекты.

Классы, интерфейсы и SOAPSuds

В клиент/серверных примерах, которые были выполнены до сих пор, мы всегда ссылались на удаленные объекты в клиентском приложении. В этом случае копируется код CIL удаленного объекта, хотя нужны только метаданные. Также невозможно, чтобы клиент и сервер программировались независимо. Значительно лучшим способом является использование вместо этого интерфейсов или утилиты SoapSuds.exe.

Интерфейсы

Мы имеем четкое разделение клиентского и серверного кода с помощью интерфейсов. Интерфейс просто определяет методы без реализации. Мы отделяем контакт между клиентом и серверов от реализации. Вот необходимые шаги для использование интерфейса:

1. Определить интерфейс, который будет помещен в сборку.

2. Реализовать интерфейс в классе удаленного объекта. Чтобы сделать это, необходимо сослаться на сборку интерфейса.

3. На серверной стороне не требуется больше никаких изменений. Сервер можно программировать и конфигурировать обычным образом.

4. На клиентской стороне сошлитесь на сборку интерфейса вместо сборки удаленного объекта.

5. Клиент может теперь использовать интерфейс удаленного объекта, а не класс удаленного объекта. Объект можно создать с помощью класса Activator, как это делалось ранее. Нельзя при этом использовать new, так как невозможно создать экземпляр самого интерфейса.

Интерфейс определяет контракт между клиентом и сервером. Два приложения могут теперь разрабатываться независимо друг от друга. Если при этом придерживаться старых правил COM об интерфейсах (что интерфейсы никогда не должны меняться), то не будет никаких проблем с версиями.

SOAPSuds

Можно также использовать утилиту soapsuds, чтобы получить метаданные из сборки, soapsuds преобразовывает сборки в XML Schemas, XML Schemas для погружения классов и другие директивы.

Следующая команда преобразует тип Hello из сборки RemoteHello.dll в сборку HelloWrapper.dll, где генерируется прозрачный прокси, который вызывает удаленный объект.

soapsuds -types:Wrox.ProfessionalCSharp.Hello, RemoteHello -oa:HelloWrapper.dll

С помощью soapsuds можно также получить информацию о типах непосредственно с выполняющегося сервера:

soapsuds -url:http://localhost:6792/hi -oa:HelloWrapper.dll

На клиенте теперь можно ссылаться вместо исходной сборки на созданную soapsuds сборку. Некоторые из возможностей перечислены в таблице.

Параметр

Описание

-url

Извлекает схему из указанного URL.

-proxyurl

Если требуется прокси сервер для доступа к серверу, определите прокси с помощью этого параметра.

-types

Определяет тип и сборку для чтения из нее информации о схеме.

-is

Файл ввода схемы.

-ia

Файл ввода сборки.

-os

Файл вывода схемы.

-oa

Файл вывода сборки.

Отслеживание служб

Отладку и поиск неисправностей в приложениях, использующих .NET, можно проводить через службы удаленного отслеживания. Класс System.Runtime.Remoting.Services.TrackingService предоставляет службу слежения для получения информации о том, когда происходит маршализация и демаршализация, когда вызываются удаленные объекты и разъединяются и т. д.

□ С помощью служебного класса TrackingServices регистрируется и отменяется регистрация обработчика, который реализует ITrackingHandler.

□ Интерфейс ITrackingHandler вызывается, когда на удаленном объекте или на прокси происходит событие. Можно реализовать три метода в обработчике: MarshaledObject(), UnmarshaledObject() и DisconnectedObject().

Чтобы увидеть службы слежения в действии на клиенте и на сервере, создадим новую библиотеку классов TrackingHandler. Класс TrackingHandler реализует интерфейс ITrackingHandler. В методах задаются два аргумента: сам объект и ObjRef. С помощью ObjRef выдается информация об URI, канале и уполномоченных приемниках. Можно также присоединить новые приемники для добавления спонсоров всех вызываемых методов. В данном примере на консоль записывается URI и информация о канале.

using System;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Services;

namespace Wrox.ProfessionalCSharp (

public class TrackingHandler : ITrackingHandler {

public TrackingHandler() {

}

public void MarshaledObject(object obj, ObjRef or) {

Console.WriteLine("--- Marshaled Object " + obj.GetType() + " ---");

Console.WriteLine("Object URI: " + or.URI);

object[] ChannelData = or.ChannelInfo.ChannelData;

foreach(object data in ChannelData) {

ChannelDataStore dataStore = data as ChannelDataStore;

if (dataStore != null) {

foreach (string uri in dataStore.ChannelUris) {

Console.WriteLine("Channel URI: " + uri);

}

}

}

Console.WriteLine("---------");

Console.WriteLine();

}

public void UnmarshaledObject(object obj, ObjRef or) {

Console.WriteLine("Unmarshal");

public void DisconnectedObject(Object obj) {

Console.WriteLine("Disconnect");

}

}

}

Серверная программа изменяется, чтобы регистрировать TrackingHandler. Необходимо добавить только две строки, чтобы зарегистрировать обработчик.

using System.Runtime.Remoting.Services;

// ...

public static void Main(string[] args) {

TrackingServices.RegisterTrackingHandler(new TrackingHandler());

TCPChannel channel = new TCPChannel(8086);

// ...

При запуске сервера первый экземпляр создается во время регистрации хорошо известного типа, и мы получаем следующий вывод. Вызывается MarshaledObject() и выводит тип объекта для маршализации — Wrox.ProfessionalCSharp.Hello. С помощью Object URI мы видим GUID, который, используется внутренне в удаленной среде выполнения для различения определенных экземпляров и URI. С помощью канала URI можно проверить конфигурацию канала. В этом случае именем хоста будет Cnagel:

Асинхронная удаленная работа

Если серверные методы требуют времени для завершения работы и клиенту нужно произвести некоторую другую работу в это время- то необходимо запустить отдельный поток выполнения, чтобы сделать удаленный вызов. Асинхронные вызовы могут делаться на удаленном объекте так же, как они делаются на локальном объекте.

Чтобы сделать асинхронный метод, создается делегат GreetingDelegate с тем же аргументом и возвращается значение как метод Greeting() удаленного объекта. Аргумент этого делегата является ссылкой на метод Greeting(). Мы запускаем вызов Greeting(), используя метод делегата BeginInvoke(). Второй аргумент BeginInvoke() является экземпляром: AsyncCallback, определяющим метод НеlloClient.Callback(), который вызывается когда удаленный метод заканчивается. В методе Callback() удаленный вызов заканчивается с помощью EndInvoke():

using System;

using System.Runtime.Remoting;

namespace Wrox.ProfessionalCSharp {

public class HelloClient {

private delegate String GreetingDelegate(String name);

private statiс string greeting; public static old Main(string[] args) {

RemotingConfiguration.Configure("HelloClient.exe.config");

Hello obj = new Hello();

if (obj == null) {

Console.WriteLine("could not locate server");

return 0;

}

// синхронная версия

// string greeting = obj.Greeting("Christian");

// асинхронная версия

GreetingDelegate d = new GreetingDelegate(obj.Greeting);

IAsyncResult ar = d.BeginInvoke("Christian", null, null);

// выполнить некоторую работу и затем ждать

ar.AsyncWaitHandle.WaitOne();

if (ar.IsCompleted) {

greeting = d.EndInvoke(ar);

}

Console. WriteLine(greeting);

}

}

}

О событиях, делегатах и асинхронных методах можно прочитать в главе 6.

Атрибут OneWay

Метод, который возвращает void и имеет только входящие параметры, может быть помечен атрибутом OneWay. Атрибут OneWay делает метод автоматически асинхронным независимо от того, как вызывает его клиент. Добавление метода TakeAWhile() в класс удаленного объекта RemoteHello соответствует созданию метода "породить и забыть". Если клиент вызывает его через прокси, то прокси немедленно возвращает управление клиенту. На сервере метод заканчивается немного позже:

[OneWay]

public Void TakeAWhile(int ms) {

Console.WriteLine("TakeAWhile started");

System.Threading.Thread.Sleep(ms);

Console.WriteLine("TakeAWhile finished");

}

Удаленное выполнение и события

С помощью .NET Remoting не только клиент может вызывать методы на удаленном объекте через сеть, но и сервер может также вызывать методы на клиенте. Для этого используются детали механизма основных свойств языка: делегаты и события.

В принципе это простая архитектура. Сервер имеет объект, который может вызывать клиент, а клиент имеет объект, который в свою очередь может вызывать сервер:

□ Удаленный объект на сервере должен объявить внешнюю функцию (делегата) с сигнатурой метода, который клиент будет реализовывать в обработчике.

□ Аргументы, которые передаются клиенту с функцией-обработчиком, должны быть маршализуемыми. Поэтому все посылаемые клиенту данные должны быть сериализуемыми.

□ Удаленный объект должен также объявить экземпляр функции делегата, модифицированный ключевым словом event. Клиент будет использовать его для регистрации обработчика.

□ Клиент должен создать объект приемника с методом обработчика, имеющий такую же сигнатуру, как и определенный делегат, и должен зарегистрировать объект приемника для события на удаленном объекте.

Чтобы понять это, рассмотрим пример. Создадим пять классов для всех частей обработки событий в .NET Remoting. Класс Server является удаленным сервером, таким как один из тех, которые уже до этого встречались. Класс Server будет создавать канал на основе информации из конфигурационного файла и регистрировать удаленный объект, который реализуется в классе RemoteObject в удаленной среде выполнения. Удаленный объект объявляет аргументы делегата и порождает события в зарегистрированных функциях обработчика. Аргумент, который передается в функцию обработчика, имеет тип StatusEventArgs. Класс StatusEventArgs должен быть сериализуемым, поэтому его можно маршализовать клиенту.

Класс Client представляет клиентское приложение. Этот класс создает экземпляр класса EventSink и регистрирует метод StatusHandler() этого класса как обработчика для делегата в удаленном объекте. EventSink должен быть удаленным объектом, подобным классу RemoteClass, так как этот класс также будет вызываться через сеть.

Удаленный объект

Класс удаленного объекта реализуется в файле RemoteObject.cs. Класс удаленного объекта должен выводиться из MarshalByRefObject так, как было показано в предыдущих примерах. Чтобы сделать возможным для клиента регистрацию обработчика событий, который вызывается из удаленного объекта, необходимо объявить внешнюю функцию с помощью ключевого слова delegate. Мы объявляем делегата StatusEvent() с двумя аргументами: sender (поэтому клиент знает об объекте, который порождает событие) и переменную типа StatusEventArgs. В класс аргумента помещаем всю дополнительную информацию, которую необходимо послать клиенту.

Метод, который реализуется на стороне клиента, требует строгих ограничений. Он может иметь только входные параметры, возвращаемые типы, при этом параметры ref и out недопустимы; а типы аргументов должны быть либо [Serializable], либо удаленными (выводимыми из MarshalByRefObject):

public delegate void StatusEvent(object sender, StatusEventArgs e);

public class RemoteObject : MarshalByRefObject {

Внутри класса RemoteObject объявляется экземпляр функции делегата Status, модифицированный ключевым словом event. Клиент должен добавить обработчик событий в событие Status, чтобы получить статусную информацию из удаленного объекта:

public class RemoteObject : MarshalByRefObject {

public RemoteObject() {

Console.WriteLine("RemoteObject constructor called");

}

public event StatusEvent Status;

В методе LongWorking() проверяется, что обработчик событий регистрируется прежде, чем событие порождается с помощью Status(this, е). Чтобы удостовериться, что событие порождается асинхронно, мы получаем событие в начале метода перед выполнением Thread.Sleep() и после Sleep:

public void LongWorking(int ms) {

Console.WriteLine("RemoteObject: LongWorking() Started");

StatusEventArgs e = new StatusEventArgs("Message for Client: LongWorking() Started");

// породить событие

if (Status != null) {

Console.WriteLine("RemoteObject: Firing Starting Event");

Status(this, e);

}

System.Threading.Thread.Sleep(ms);

e.Message = "Message for Client: LongWorking() Ending"; // породить событие окончания

if (Status != null) {

Console.WriteLine("RemoteObject: Firing Ending Event");

Status(this, e);

}

Console.WriteLine("RemoteObject: LongWorking() Ending");

}

}

Аргументы событий

Мы видели в классе RemoteObject, что класс StatusEventArgs используется как аргумент для делегата. С помощью атрибута [Serializable] экземпляр этого класса может передаваться от сервера клиенту. Мы используем простое свойство типа string для пересылки клиенту сообщения:

[Serializable]

public class StatusEventArgs {

public StatusEventArgs(string m) {

message = m;

}

public string Message {

get {

return message;

}

set {

message = value;

}

}

private string message;

}

Сервер

Сервер реализуется внутри консольного приложения. Мы ожидаем только, чтобы пользователь завершил работу сервера после чтения конфигурационного файла и настройки канала и удаленного объекта:

using System;

using System.Runtime.Remoting;

namespace Wrox.ProfessionalCSharp {

class Server {

static void Main(string[] args) {

RemotingConfiguration.Configure("Server.exe.config");

Console.WriteLine("Hit to exit");

Console.ReadLine();

}

}

}

Конфигурационный файл сервера

Способ создания конфигурационного файла сервера Server.exe.config мы уже обсуждали. Существует только один важный момент. Так как клиент сначала регистрирует обработчик событий и после этого вызывает удаленный метод, то удаленный объект должен сохранять состояние клиента. Можно использовать с событиями объекты SingleCall, поэтому класс RemoteObject конфигурируется в виде активированного клиентом типа:

<configuration>

<system.runtime.remoting>

<application name="CallbackSample">

<service>

<activated type="Wrox.ProfessionalCSharp.RemoteObject, RemoteObject" />

</service>

<channels>

<channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" port="6791" />

</channels>

</application>

</system.runtime.remoting>

</configuration>

Приемник событий

Приемник событий реализует обработчик StatusHandler(), который определен в делегате. Как ранее отмечалось, метод может иметь только входные параметры и возвращать только void. Это в точности соответствует требованиям методов [OneWay], как мы видели ранее при рассмотрении асинхронной удаленной работы. StatusHandler() будет вызываться асинхронно. Класс EventSink должен также наследовать из класса MarshalByRefObject, чтобы сделать его удаленным, так как он будет вызывать с сервера удаленным образом:

using System;

using System.Runtime.Remoting.Messaging;

namespace Wrox.ProfessionalCSharp; {

public class EventSink MarshalByRefObject {

public EventSink() { }

[OneWay]

public void StatusHandler(object sender, StatusEventArgs e) {

Сonsole.WriteLine("EventSink: Event occurred: " + e.Message);

}

}

}

Клиент

Клиент читает конфигурационный файл клиента с помощью класса RemotingConfiguration. Так было со всеми клиентами, которые создавались до сих пор. Клиент создает локально экземпляр удаленного класса приемника EventSink. Метод, который должен вызываться из удаленного объекта на сервере, передается в удаленный объект:

using System;

using System.Runtime.Remoting;

namespace Wrox.ProfessionalCSharp {

class Client {

static void Main(string[] args) {

RemotingConfiguration.Configure("Client.exe.config");

Различие начинается здесь. Мы должны создать локально экземпляр удаленного класса приемника EventSink. Так как этот класс не будет конфигурироваться элементом <client>, то его экземпляр создается локально. Затем мы получаем экземпляр класса удаленного объекта RemoteObject. Этот класс конфигурируется в элементе <client>, поэтому его экземпляр создается на удаленном сервере:

EventSink sink = new EventSink();

RemoteObject obj = new RemoteObject();

Теперь можно зарегистрировать метод обработчика объекта EventSink на удаленном объекте. StatusEvent является именем делегата, который был определен на сервере. Метод StatusHandler() имеет те же самые аргументы, которые определены в StatusEvent.

Вызывая метод LongWorking(), сервер будет делать обратный вызов в методе StatusHandler() в начале и в конце метода:

// зарегистрировать клиентский приемник на сервере — подписаться

// на событие

obj.Status += new StatusEvent(sink.StatusHandler);

obj.LongWorking(5000);

Теперь мы более не заинтересованы в получении событий с сервера и отменяем подписку на событие. Следующий раз при вызове LongWorking() никакие события не будут получены.

// отменить подписку на событие

obj.Status -= new StatusEvent(sink.StatusHandler);

obj.LongWorking(5000);

Console.WriteLine("Hit to exit");

Console.ReadLine();

}

}

}

Конфигурационный файл клиента

Конфигурационный файл для клиента — client.exe.config является почти таким же конфигурационным файлом, как и для активированных клиентом объектов. Различие можно найти в определении номера порта для канала. Поскольку сервер должен соединяться с клиентом через известный порт, то необходимо определить номер порта для канала как атрибут элемента <channel>. Не требуется определять раздел <service> для класса EventSink, так как экземпляр этого класса будет создаваться клиентом локально с помощью оператора new. Сервер не получает доступ к этому объекту по его имени, вместо этого он получит маршализированную ссылку на экземпляр:

<configuration>

<system.runtime.remoting>

<application name="Client">

<client url="http://localhost:6791/CallbackSample">

<activated type="Wrox.ProfessionalCSharp.RemoteObject, RemoteObject" />

</client>

<channels>

<channel type="System.Runtime.Remoting.Channels.Http.HttpChannel, System.Runtime.Remoting" port="777" />

<channels>

</application>

</system.runtime.remoting>

</configuration>

Выполнение программы

Мы видим результирующий вывод на сервере: конструктор удаленного объекта вызывается один раз, так как имеется активированный клиентом объект. Затем происходит вызов метода LongWorking() и порождение события на клиенте. Следующий запуск метода LongWorking() не порождает событий, так как клиент уже отменил регистрацию своего интереса к событию:

В выводе клиента видно, что события достигают его по сети:

Контексты вызова

Активированные клиентом объекты могут сохранять состояние для определенного клиента. Для активированных клиентом объектов на сервере требуются ресурсы. Для активированных сервером объектов SingleCall новый экземпляр создается для каждого вызова экземпляра и никакие ресурсы не удерживаются на сервере, эти объекты не могут хранить состояние для клиента. Для управления состоянием можно держать состояние на клиентском стороне, данные о состоянии этого объекта посылаются с каждым вызовом метода на сервер. Для передачи состояния не требуется изменять сигнатуры всех методов с целью включения дополнительного параметра, так как можно использовать контексты вызова.

Контекст вызова перемещается вместе с логическим потоком выполнения и передается с каждым вызовом метода. Логический поток выполнения запускается из вызывающего потока выполнения и перемещается через все вызовы методов, которые запускаются из вызывающего потока выполнения и передаются через различные контексты, различные домены приложений и различные процессы.

Можно присвоить данные контексту вызова с помощью метода CallContext.SetData(). Класс с объекта, который используется в качестве данных для метода SetData(), должен реализовать интерфейс ILogicalThreadAffinative. Эти данные можно получить снова в том же логическом потоке выполнения (но, возможно, в другом физического потоке выполнения) с помощью CallContext.GetData().

Для данных контекста вызова здесь создается новая библиотека классов C# с вновь созданным классом CallContextData. Этот класс будет использоваться для передачи некоторых данных от клиента серверу с каждым вызовом метода. Класс, который передается с контекстом вызова, должен реализовать интерфейс System.Runtime.Remoting.Messaging.ILogicalThreadAffinative. Этот интерфейс не имеет метода, это просто отметка для среды выполнения, определяющая, что экземпляры этого класса перемещаются вместе с логическим потока выполнения. Класс CallContextData также помечается атрибутом Serializable, чтобы он мог передаваться по каналу:

using System;

using System.Runtime.Remoting.Messaging

namespace Wrox.ProfessionalCSharp {

[Serializable]

public class CallContextData : ILogicalThreadAffinative {

public CallContextData() { }

public string Data {

get {

return data;

}

set {

data = value;

}

}

protected string data;

}

}

В классе Hello метод Greeting() изменяется так, чтобы можно было получить доступ к контексту вызова. Для использования класса CallContextData необходимо сослаться на созданную ранее сборку CallContextData.dll. Чтобы работать с классом CallContext, должно быть открыто пространство имен System.Runtime.Remoting.Messaging:

public string Greeting(string name) {

Console.WriteLine("Greeting started");

CallContextData cookie = (CallContextData)CallContext.GetData("mycookie");

if (cookie ! = null) {

Console.WriteLine("Cookie: " + cookie.Data);

}

Console.WriteLine("Greeting finished");

return "Hello, " + name;

}

В клиентском коде передается информация контекста вызова:

CallContextData cookie = new CallContextData();

cookie.Data = "information for the server";

CallContext.SetData("mycookie", cookie);

for (int i=0; i< 5; i++) {

Console.WriteLine(obj.Greeting("Christian"));

}

Такой контекст вызова может использоваться для отправки информации о пользователе, имени клиентской системы или просто как уникальный идентификатор, который применяется на серверной стороне для получения из базы данных некоторой информации о состоянии.

Заключение

В этой главе мы видели, что .NET Remoting использовать очень легко. Удаленный объект должен просто наследовать из объекта MarshalByRefObject. В серверном приложении требуется только один метод для загрузки конфигурационного файла, чтобы настроить и запустить каналы и удаленные объекты. На клиенте загружается конфигурационный файл и используется оператор new для создания экземпляра удаленного объекта.

Также не слишком много работы требуется и в случаях, когда конфигурационные файлы не используются. На сервере создается канал и регистрируется удаленный объект. На клиенте делается канал и используется удаленный объект.

Наравне с этими приемами применяют много механизмов из других частей .NET Framework, которые также работают с .NET Remoting, такие как вызов асинхронных методов, выполнение обратных вызовов с помощью ключевых слов delegate и event и т. д.

Таким образом, использование .NET Remoting является очень простым, архитектура достаточно гибкой и по желанию расширяемой. Можно использовать каналы HTTP и TCP, которые также расширяются, или написать новые каналы с самого начала. Существуют форматтер SOAP и двоичный форматтер, но легко можно использовать свой собственный. Также имеется много точек перехвата, где возможно добавление в классы специальной функциональности, которая доставляется с помощью .NET Framework.

Глава 24

Службы Windows

В главе 22 рассматривается работа в сети, глава 23 охватывает работу с серверами с помощью .NET Remoting. Описанные серверные процессы запускаются вручную. Однако программы должны начинать работать автоматически во время запуска машины. Здесь на помощь приходят службы Windows.

В этой главе мы рассмотрим следующие вопросы:

□ Архитектура служб Windows, функциональность служебной программы, служебная управляющая программа, и служебная конфигурационная программа.

□ Реализация службы с помощью классов, находящихся в пространстве имен System.ServiceProcess.

□ Программы установки для конфигурирования службы в реестре.

□ Написание программы для управления службой с помощью класса ServiceController.

□ Реализация обработки событий. Так как службы обычно не имеют интерфейса пользователя, ошибки не могут выводиться в окне сообщений. Обработка событий является хорошим способом сообщения об ошибках.

□ Производительность службы. Мониторинг производительности может использоваться для получения информации о нормальном выполнении службы.

Вначале мы рассмотрим, что же такое службы.

Понятие службы

Службы Windows являются приложениями, которые начинают работать автоматически при запуске операционной системы. Они могут выполняться без интерактивного взаимодействия с пользователем системы. Можно сконфигурировать службу под требования специально сконфигурированным пользователем или таким пользователем System, который имеет больше привилегий, чем системный администратор.

Службы не выполняются на Windows 98 или Windows ME. Для них требуется ядро NT. Службы Windows работают в Windows NT 4, Windows 2000 и Windows ХР.

Вот несколько примеров таких служб:

□ Простая служба TCP/IP является служебной программой, которая содержит несколько небольших серверов TCP/IP: echo, daytime, quote и других.

□ Служба публикации в Web является службой Информационного сервера Интернета (IIS).

□ Журнал событий является службой для регистрации сообщений в системе регистрации событий.

□ Microsoft Search является службой, которая создает индексы данных на диске.

Можно использовать административную утилиту Component Services (Службы компонентов) для просмотра всех служб в системе. В Windows 2000 Server эта программа доступна через Start|Programs|Administrative Tools|Services:

Архитектура

Три типа программ требуются для работы службы. Служебная программа предназначена для решения реальной проблемы, в ней необходимо закодировать реальную функциональность. С помощью служебной управляющей программы можно посылать службе управляющие запросы, такие как запуск, останов, пауза и продолжение. И последнее, но не менее важное — требуется конфигурационная программа службы, с помощью которой можно установить службу. Это означает, что она не только копируется в файловую систему, но записывается также в реестр и конфигурируется как служба. В то время как компоненты .NET можно устанавливать, делая хсору, так как им не требуется реестр, установке служб необходима конфигурация в реестре. Программа конфигурации службы может также использоваться для последующего изменения конфигурации службы.

Служебная программа

Прежде чем рассматривать реализацию службы в .NET, давайте выясним, на что похожа архитектура служб в Windows и какова внутренняя функциональность службы.

Служебная программа реализует функциональность службы. Ей требуются три части: основная функция (точка входа программы), основная служебная функция и обработчик. Service Control Manager (SCM) играет очень важную роль для служб, он посылает запросы службе для ее запуска и останова. В служебной программе необходимо регистрировать точки входа службы в SCM, чтобы SCM мог вызывать эти точки входа в службе.

В основной функции служебной программы точки входа для основных служебных функций должны регистрироваться в SCM. SCM требуется информация об основных служебных функциях, чтобы эти функции можно было вызывать в SCM при запуске службы.

Основная функция может зарегистрировать более одной основной служебной функции. Она должна регистрировать основную служебную функцию для каждой предоставляемой службы. Служебная программа может предоставить множество служб в одной программе. Например, C:\winnt\system32\services.exe является служебной программой, которая включает Alerter, Application Management, Computer Browser, DHCP Client, Distributed Link Tracking Client and Server, DNS Client, Event Log, и некоторые другие службы.

Второй частью служебной программы является основная служебная функция, которая содержит функциональность службы. Эта функция вызывается SCM, когда служба должна запускаться. Служба World Wide Publishing запускает поток выполнения, который слушает обычно порт 80 и ожидает запросы HTTP. Клиент DHCP запрашивает, освобождает и обновляет динамически присвоенные адреса IP. Основная функциональность службы находится внутри основной служебной функции. У основной служебной функции существует еще одна обязанность в отношении регистрации другой точки входа в SCM: эта функция должна регистрировать функцию обработки в SCM.

Функция обработки является третьей частью служебной программы. Обработчик должен отвечать на события из SCM. Службы могут останавливаться, приостанавливаться, продолжаться. Обработчик должен реагировать на эти события.

Управляющий менеджер служб

Управляющий менеджер служб (SCM — Service Control Manager) является частью операционной системы, которая взаимодействует со службой. Давайте посмотрим, как работает эта коммуникация на диаграмме последовательностей UML:

Во время начальной загрузки системы начинает работу каждый процесс, для которого задан автоматический запуск службы, и поэтому вызывается основная функция этого процесса. Служба должна зарегистрировать основную функцию для каждой из своих служб, затем SCM вызывает основную служебную функцию. Основная служебная функция несет на себе, как ранее сообщалось, основную функциональность службы.

Одной из важных задач, которую имеет основная служебная функция, является регистрация обработчика в SCM. Служебная управляющая программа посылает запросы SCM для остановки, приостановки и возобновления работы службы. Служебная управляющая программа независима от SCM и самой службы. Мы получаем вместе с операционной системой множество служебных управляющих программ, одна из них — ММС Services Snap-in, которую мы видели ранее. Можно написать также свою собственную служебную управляющую программу. Хорошей служебной управляющей программой является часть установки SQL Server. Она выводит цветные кнопки для управления службами SQL Server:

Служебная управляющая программа

Как предполагает название, служебная управляющая программа управляет службой. Для остановки, приостановки и возобновления работы службы ей посылаются управляющие коды, а обработчик должен реагировать на эти события. Также можно запрашивать службу о реальном статусе, при этом реализуется специальный обработчик, который отвечает на специальные управляющие коды.

Конфигурационная программа службы

Мы не можем использовать для установки службы хсору, так как службы должны конфигурироваться в реестре. Можно задать тип запуска как автоматический, ручной или отключенный. Необходимо сконфигурировать пользователя служебной программы и зависимости службы, например службы, которые должны начать работу перед тем, как запускается эта служба. Все конфигурации производятся внутри конфигурационной программа службы. Установка программы использует программу для конфигурирования службы, но эта программа может также использоваться позже для изменения параметров конфигурации службы.

Пространство имен System.ServiceProcess

В .NET Framework находятся классы служб в пространстве имен System.ServiceProcess, которые запускают три части службы:

□ Для реализации службы мы наследуем из класса ServiceBase который используется для регистрации служб и отвечает на запросы запуска и останова.

□ Класс ServiceController используется для реализации служебной управляющей программы. С помощью этого класса можно посылать запросы службам.

□ Классы ServiceProcessInstaller и ServiceInstaller являются, как предполагают их имена, классами для установки и конфигурирования служебных программ.

Давайте посмотрим, как создается новая служба

Создание службы

Создаваемая служба будет содержать сервер цитирования (quote server). Для каждого сделанного клиентом запроса сервер цитирования возвращает случайную цитату файла цитат. Первая часть решения будет сделана с помощью трех сборок: одна для клиента и две — для сервера. Сборка QuoteServer содержит реальную функциональность. Мы прочитаем файл цитат в кэш памяти и будем отвечать на запросы цитат с помощью сервера сокета.

QuoteСlient является клиентским приложением Windows Forms. Это приложение создает клиентский сокет для коммуникации с QuoteServer. Третья сборка является реальной службой. QuoteService запускает и останавливает QuoteServer, служба будет управлять сервером:

Прежде чем разработать служебную часть программы, создадим простой сервер сокета в дополнительной библиотеке классов C#, которая будет использоваться из процесса службы.

Библиотека классов, использующая сокеты

В системе Windows 2000 Server можно установить службы простого TCP/IP как компоненты Windows. Частью служб простого TCP/IP является сервер TCP/IP "цитата дня" ("quote of the day", кратко называемая "qotd"). Эта простая служба слушает порт 17 и отвечает на каждый запрос случайным сообщением из файла c:\winnt\system32\drivers\etc\quotes. Здесь будет создан аналогичный сервер. Только этот сервер возвращает строку Unicode, в отличие от старого qotd, который возвращает код ASCII.

Постепенно начнем создавать исходный код класса QuoteServer в файле QuoteServer.cs:

using System;

using System.IO;

using System.Threading;

using System.Net.Sockets;

using System.Text;

using System.Collections.Specialized;

namespace Wrox.ProfessionalCSharp {

/// <summary>

/// Пример сервера сокета.

/// </summary>

public class QuoteServer {

private TcpListener listener;

private int port;

private string filename;

private StringCollection quotes;

private Random random;

private Thread listenerThread;

Конструктор QuoteServer() является перезагруженным, чтобы имя файла и порт можно было передать в вызове. Конструктор, где передается только имя файла, использует для сервера по умолчанию порт 7890. Конструктор по умолчанию определяет имя файла по умолчанию для цитат как quotes.txt:

public QuoteServer() : this("quotes.txt") {}

public QuoteServer(string filename) : this(filename, 7890) {}

public QuoteServer(string filename, int port) {

this.filename = filename;

this.Port = port;

}

ReadQuotes() является вспомогательным методом, который читает все цитаты из файла, который был определен в конструкторе. Все цитаты добавляются в StringCollection quotes. Кроме того создается экземпляр класса Random, который будет использоваться для возврата случайных цитат:

protected void ReadQuotes() {

quotes = new StringCollection();

Stream stream = File.OpenRead(filename);

StreamReader StreamReader = new StreamReader(stream);

string quote;

while ((quote = streamReader.ReadLine()) != null) {

quotes.Add(quote);

}

streamReader.Close(); stream.Close();

random = new Random();

}

Другим вспомогательным методом является GetRandomQuoteOfTheDay(). Этот метод возвращает случайную цитату из цитат StringCollection quotes:

protected string GetRandomQuoteOfTheDay() {

int index = random.Next(0, quotes.Count); return quotes[index];

}

В методе Start() весь файл, содержащий цитаты, в StringCollection quotes считывается с помощью вспомогательной функции ReadQuotes(). После этого запускается новый поток выполнения, который незамедлительно вызывает метод Listener(). Мы используем поток выполнения, так как метод Start() не может блокироваться и ждать клиента, он должен сразу же вернуть управление вызывающей стороне (SCM). SCM предположит, что запуск отказал, если метод не вернет своевременно управление вызывающей стороне.

public void Start() {

ReadQuotes();

listenerThread = new Thread(new ThreadStart(this.Listener));

listenerThread.Start();

}

Функция потока выполнения Listener() создает экземпляр TCPListener. В методе AcceptSocket() ожидается соединение клиента. Как только клиент соединяется, AcceptSocket() возвращает управление с сокетом, связанным с клиентом. Метод GetRandomQuoteOfTheDay() вызывается для отправки возвращаемой случайной цитаты клиенту с помощью socket.Send():

protected void Listener() {

listener = new TcpListener(port);

listener.Start();

while (true); {

Socket socket = listener.AcceptSocket();

if (socket == null) {

return;

}

string message = GetRandomQuoteOfTheDay();

UnicodeEncoding encoder = new UnicodeEncoding();

byte [] buffer = encoder.GetBytes(message);

socket.Send(buffer, buffer.Length, 0);

socket.Close();

}

}

Помимо Start() существуют другие методы для управления службой: Stop(), Suspend() и Resume():

public void Stop() {

listener.Stop();

}

public void Suspend() {

listenerThread.Suspend();

}

public void Resume() {

listenerThread.Resume();

}

Методом, который будет открыто доступным, является RefreshQuotes(). Если содержащий цитаты файл изменяется, то запускается повторное чтение данного файла с помощью этого метода:

public void RefreshQuotes() {

ReadQuotes();

}

}

}

Прежде чем разрабатывать службу для нашего сервера, полезно будет создать тестовую программу, которая имеет экземпляр QuoteServer и вызывает Start(). Таким образом можно протестировать функциональность без необходимости обрабатывать специфические для службы вопросы. Можно сконцентрироваться на создании требуемой функциональности. Этот тестовый сервер должен запускаться вручную и код легко просматривается с помощью отладчика.

Тестовая программа является консольным приложением C#. Мы ссылаемся на сборку класса QuoteServer. Содержащий цитаты класс копируется в каталог с:\wrox (или нужно изменить аргумент конструктора для определения, куда копируется файл). После вызова конструктора мы обращаемся к методу Start() экземпляра QuoteServer.Start() возвращает управление сразу после создания потока выполнения, поэтому консольное приложение продолжает выполняться до тех пор, пока не будет нажата клавиша Return:

static void Main(string[] args) {

QuoteServer qs = new QuoteServer(@"c:\wrox\quotes.txt", 4567);

qs.Start();

Console.WriteLine("Hit return to exit");

Console.ReadLine();

qs.Stop();

}

Отметим, что QuoteServer с помощью этой программы будет выполняться на порте 4567 на localhost и необходимо использовать эти настройки позже на клиенте.

Пример TcpClient

Клиент является простым приложением Windows, где можно вводить имя хоста и номер порта сервера. Это приложение использует класс TCPClient для соединения с функционирующим сервером и получения возвращаемого сообщения для вывода его в текстовом поле. Внизу формы выводится статусная строка:

В этом коде используются инструкции using:

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

using System.Net;

using System.Net.Sockets;

using System.Text;

Мы также включаем ссылку на файл QuoteServer.dll. Оставшаяся часть кода автоматически создается в IDL, поэтому он здесь не будет рассматриваться подробно. Основная функциональность клиента находится в обработчике нажатия кнопки Get Quote:

protected void buttonQuote_Click(object sender, System.EventArgs e) {

statusBar.Text = "";

string server = textBoxHostname.Text;

try {

int port = Convert.ToInt32(textBoxPortNumber.Text);

} catch (FormatException ex) {

statusBar.Text = ex.Message; return;

}

TcpClient client = new TcpClient();

try {

client.Connect(

textBoxHostname.Text, Convert.ToInt32(textBoxPortNumber.Text));

NetworkStream stream = client.GetStream();

byte[] buffer = new Byte[1024];

int received = stream.Read(buffer, 0, 1024);

if {received <= 0) {

statusBar.Text = "Read failed"; return;

}

texBoxQuote.Text = Encoding.Unicode.GetString(buffer);

} catch (SocketException ex) {

statusBar.Text = ex.Message;

} finally {

client.close();

}

}

Запустив тестовый сервер и клиент этого оконного приложения, можно протестировать функциональность. Успешное выполнение может дать следующий результат при использовании указанных на экране настроек:

Добавим серверу функциональность службы. Программа уже выполняется, что же нужно еще сделать? Необходимо, чтобы серверная программа автоматически запускалась во время начальной загрузки системы без какого-либо пользователя, зарегистрировавшегося в системе, и мы хотим управлять ею с помощью служебных управляющих программ.

Проект Windows Service

Используя новый мастер проектов для C# Windows Services, можно теперь начать создавать службу Windows. Будьте внимательны, чтобы не выбрать проект Web Service.

Службы Web рассматриваются в главе 17.

После нажатия OK для создания приложения Windows Service появится окно проектировщика, как в приложениях Windows Forms, но здесь нельзя вставлять компоненты Windows Forms. Окно проектировщика будет использоваться для добавления других компонентов, таких как счетчики производительности и регистрации событий: Выбор свойств этой службы открывает окно редактора свойств:

Сконфигурируем свойства службы:

□ AutoLog означает, что события автоматически регистрируются для запуска и остановки службы.

□ CanPauseAndContinue, CanShutdown и CanStop означают, что служба может обрабатывать специальные запросы pause, continue, shutdown и stop.

□ ServiceName является именем службы, которое записывается в реестр и используется для управления службой.

□ CanHandlePowerEvent является допустимым параметром для служб, работающих на системе Windows 2000. Мы поговорим о параметрах power позже.

По умолчанию используется имя службы WinService1 независимо от названия проекта. Можно установить только одну службу WinService1. Если возникает ошибка установки во время процесса тестирования, то причина может быть в этом. Проверьте, что имя службы изменено на более подходящее в начале разработки службы.

Изменение этих свойств с помощью редактора свойств задает значения нашего класса, производного из ServiceBase, в методе InitializeComponent(). Мы уже знаем этот метод из приложений Windows Forms. Со службами он используется аналогичным образом.

Мастер создаст код, но мы изменим имя файла на QuoteService.cs, имя пространства имен на Wrox.ProfessionalCSharp, а имя класса на QuoteService. Мы подробно рассмотрим этот код позже, а пока остановимся на классе ServiceBase.

Класс ServiceBase

Класс ServiceBase является базовым для всех служб .NET. Наш класс QuoteService выводится из ServiceBase. Этот класс общается со служебным управляющим менеджером при помощи недокументированного вспомогательного класса System.ServiceProcess.NativeMethods, который служит просто классом-оболочкой для вызовов API Win32. Этот класс закрытый, поэтому мы не можем его использовать.

Следующая диаграмма последовательностей показывает взаимодействие SCM — класса QuoteService и классов из пространства имен System.ServiceProcess. На диаграмме последовательностей просматриваются вертикальные линии жизни объектов и коммуникация, проходящая в горизонтальном направлении. Коммуникация упорядочена по времени сверху вниз:

SCM запускает процесс службы. Вначале вызывается метод Main(). В методе Main() нашей службы вызывается метод Run() базового класса ServiceBase. Run() регистрирует метод ServiceMainCallback() с помощью NativeMethods.StartServiceCtrlDispatcher() в SCM и записывает вход в журнал событий.

Следующий шаг — SCM вызывает зарегистрированный метод ServiceMainCallback() в нашей программе службы. ServiceMainCallback сам регистрирует обработчик с помощью NativeMethods.RegisterServiceCtrlHandler[Ex]() и задает статус службы в SCM. Затем вызывается метод OnStart(), в котором мы должны реализовать код запуска. Если OnStart() выполнился успешно, то значение ресурса для StartSuccessful записывается в журнал событий.

Обработчик реализуется в методе ServiceCommandCallback(). SCM вызывает этот метод, когда служба запрашивает изменения. Метод ServiceCommandCallback() направляет запросы далее в OnPause(), OnContinue(), OnStop(), OnCustomCommand() и OnPowerEvent().

Основная функция

Рассмотрим сгенерированную основную функцию служебного процесса. В ней объявляются массив ServiceToRun классов ServiceBase. Один экземпляр класса QuoteService создается и передается как первый элемент массива ServiceToRun. Если должно выполняться более одной службы внутри этого служебного процесса, то необходимо добавить в массив больше экземпляров специальных служебных классов. Этот массив передается затем в статический метод Run() класса ServiceBase. С помощью метода Run() из ServiceBase мы задаем ссылки SCM для точек входа служб. Основной поток выполнения служебного процесса теперь заблокирован и ожидает завершения службы.

Вот автоматически сгенерированный код:

// Основная точка входа для процесса

static void Main() {

System.ServiceProcess.ServiceBase[] ServicesToRun;

// Более одной службы пользователя может выполняться в одном процессе.

// Чтобы добавить другую службу в этот процесс, измените следующую

// строку для создания второго служебного объекта. Например:

// ServiceToRun = New System.ServiceProcess.ServiceBase[]

// {

// new WinService1(), new MySecondUserService()

// };

ServiceToRun = new System.ServiceProcess.ServiceBase[] {

new QuoteService()

};

System.ServiceProcess.ServiceBase.Run(ServiceToRun);

}

При наличии только одной службы можно удалить в процессе массив. Метод Run() получает один объект, производный из класса ServiceBase, поэтому функцию Main можно сократить до приведенного здесь кода:

System,ServiceProcess.ServiceBase.Run(new QuoteService());

Если существует более одной службы и для них требуется некоторая общая инициализация, то эта общая инициализация должна делаться до метода Run(), так как основной поток выполнения блокируется, пока служебный процесс не будет остановлен. Последующие инструкции остаются недоступными до конца службы.

Инициализация не может продолжаться слишком долго, время выполнения не должно превышать 30 с. Если код инициализации будет более длительным, то служебный управляющий менеджер предположит, что запуск службы отказал. Но необходимо принимать в расчет самые медленные машины, где эта служба также должна выполняться с учетом ограничения в 30 с. Если процесс продолжается дольше, можно запустить инициализацию в другом потоке выполнения, чтобы основной поток выполнения вызывал Run() вовремя. Объект события может затем использоваться для сигнализации того, что поток выполнения закончил свою работу.

Запуск службы

При запуске службы вызывается метод OnStart(). Здесь может начать работу сервер сокета. Чтобы использовать QuoteServer, должна быть указана сборка Quote.Server.dll. Поток выполнения, вызывающий OnStart(), не может быть блокирован, этот метод возвращает управление вызывающей стороне, которая является методом ServiceMainCallback() класса ServiceBase. Класс ServiceBase регистрирует обработчик и информирует SCM после вызова OnStart(), что служба успешно запущена:

/// <summary>

/// Задает вещи по ходу, чтобы служба могла делать свою работу.

/// </summary>

protected override void OnStart(string[] args) {

quoteServer = new QuoteServer(@"c:\Wrox\quotes.txt", 5678);

quoteServer.Start();

}

Переменная quoteServer объявляется как закрытый член класса:

namespace Wrox.ProfessionalCSharp {

public class QuoteService : System.ServiceProcess.ServiceBase {

/// <summary>

/// Требуемые переменные проектировщика.

/// </summary>

private System.ComponentModel.Container components;

private QuoteServer quoteServer;

Методы обработки

Когда служба заканчивает работу, вызывается метод OnStop(). Необходимо остановить функциональность службы в этом методе:

/// <summary>

/// Остановить эту службу. /// </summary>

protected override void OnStop() {

quoteServer.Stop();

}

В дополнение к OnStart() и OnStop() можно переопределить в классе следующие обработчики:

□ OnPause() вызывается, когда служба должна быть временно остановлена.

□ OnContinue() вызывается, когда служба возвращается к нормальной работе после временной остановки. Чтобы сделать возможным вызов перезагруженных методов OnPause() и OnContinue(), свойство CanPauseAndContinue должно быть задано как true.

□ OnShutdown() вызывается, когда Windows осуществляет выключение системы. Обычно поведение этого метода аналогично реализации OnStop(): если для выключения потребуется больше времени, то запрашивается дополнительное время. Аналогично OnPause() и OnContinue имеется свойство, чтобы включить такое поведение,— CanShutdown, которое должно быть задано как true.

□ OnCustomCommand() является обработчиком, который обслуживает специальные команды. С помощью специальной служебной управляющей программы службе посылаются особые команды. Как эти команды обрабатываются, определяет реализация OnCustomCommand(). Этот метод имеет аргумент типа int, где задается номер специальной команды. Значение может быть в диапазоне от 128 до 256, значения меньше 128 являются зарезервированными системой значениями. В нашей службе повторное чтение файла цитат выполняется с помощью специальной команды 128:

protected override void OnPause() {

quoteServer.Suspend();

}

protected override void OnContinue() {

quoteServer.Resume();

}

protected override void OnShutDown() {

OnStop();

}

public const int commandRefresh = 128;

protected override void OnCustomCommand(int command) {

switch(command) {

case CommandRefresh:

quoteServer.RefreshQuotes();

break;

default:

break;

}

}

Как и раньше, необходимо добавить ссылку на файл QuoteServer.dll.

Потоки выполнения и службы

При использовании служб мы имеем дело с потоками выполнения. Как мы говорили ранее, SCM предполагает, что служба отказала, если инициализация продолжается слишком долго. Чтобы справиться с этим, необходимо создать поток выполнения. Метод OnStart() в служебном классе должен вернуть управление вовремя. Для вызова заблокированного метода, такого как AcceptSocket() из класса TopListener, необходимо запустить поток выполнения. Если мы не находимся внутри AcceptSocket(), то следующий клиент, запрашивающий службу, должен ожидать, пока мы там не окажемся. Это означает, что если для клиента нужно сделать некоторую работу, то используется пул потоков выполнения.

Установка службы

Служба должна конфигурироваться в реестре. Все службы можно найти в HKEY_LOCAL_MACHINE\System\CurrentControlSetServices. Записи реестра можно увидеть с помощью regedit. Там находятся тип службы, выводимое имя, путь доступа к исполняемому файлу, конфигурация запуска и т.д.

Эту конфигурацию можно сделать с помощью классов установки из пространства имен System.ServiceProcess.

Программы установки

Можно добавить программу установки в службу, переключаясь в представление конструктора в Visual Studio.NET и выбирая параметр Add Installer из контекстного меню. С помощью этого параметра создается новый класс ProjectInstaller и экземпляры ServiceProcessInstaller и ServiceInstaller:

Диаграмма классов установки для служб должна помочь пониманию созданного мастером кода:

Помня об этой диаграмме, пройдем через исходный код в файле ProjectInstaller.cs, созданный с помощью параметра Add Installer.

Класс Installer

Класс ProjectInstaller выводится из класса System.Configuration.Install.Installer. Класс Installer является базовым классом для всех специальных классов установки. С его помощью создается установка на основе транзакций, при которой можно вернуться в предыдущее состояние, если установка отказывает. При откате все изменения, сделанные при установке, будут отменены. Как можно видеть на диаграмме, класс Installer имеет методы Install(), Commit(), Rollback() и Uninstall(), вызываемые из программ установки.

Атрибут RunInstaller(true) означает, что при установке сборки должен вызываться класс ProjectInstaller. Специальные программы установки действий, а также утилита installutil.exe (которая будет использоваться позднее) проверяют атрибут:

using System;

using System.Collections;

using System.ComponentModel;

using System.Configuration.Install;

namespace Wrox.ProfessionalCSharp {

/// <summary>

/// Краткое описание ProjectInstaller

/// </summary>

[RunInstaller(true)]

public class ProjectInstaller : System.Configuration.Install.Installer {

Классы ServiceProcessInstaller и ServiceInstaller

Аналогично приложениям Windows Forms метод InitializeComponent() вызывается внутри конструктора класса ProjectInstaller. В методе InitializeComponent() создается экземпляр класса ServiceProcessInstaller и класса ServiceInstaller. Оба эти класса выводятся из класса ComponentInstaller, который сам является Installer.

Классы, производные из ComponentInstaller, используются как части процесса установки. Помните, что служебный процесс может включать более одной службы. Класс ServiceProcessInstaller применяется для части процесса установки, а класс ServiceInstaller для части службы, поэтому один экземпляр ServiceInstaller требуется для каждой службы. Если в процессе имеется три службы, то необходимо добавить дополнительные объекты ServiceInstaller, в таком случае понадобятся три экземпляра ServiceInstaller.

private System.ServiceProcess.ServiceProcessInstaller serviceProcessInstaller1;

private System.ServiceProcess.ServiceInstaller serviceInstaller1;

/// <summary>

/// требуемые переменные конструктора.

/// </summary>

private System.ComponentModel.Container components;

public ProjectInstaller() {

// Этот вызов затребован конструктором.

InitializeComponent();

// TODO: добавить инициализацию после вызова InitComponent

}

/// <summary>

/// Требуемый метод для поддержки конструктора — не изменяйте

/// содержимое этого метода с помощью редактора кода.

/// </summary>

private void InitializeComponent() {

this.serviceProcessInstaller1 =

new System.ServiceProcess.ServiceProcessInstaller();

this.serviceInstaller1 =

new System.ServiceProcess.ServiceInstaller();

//

// serviceProcessInstaller1

//

this.serviceProcessInstaller1.Password = null;

this.serviceProcessInstaller1.UserName = null;

//

// serviceInstaller1

//

this.serviceInstaller1.ServiceName = "QuoteService";

//

// ProjectInstaller

//

this.Installers.AddRange(

new System.Configuration.Install.Installer[] {

this.serviceProcessInstaller1, this.serviceInstaller1});

}

}

}

ServiceProcessInstaller устанавливает исполняемый файл, который реализует класс ServiceBase. ServiceProcessInstaller имеет свойства для всего процесса и для всех служб внутри процесса:

Свойства ServiceProcessInstaller

Username, Password

Указывают учетную запись пользователя, с которой выполняется служба, если свойство RunUnderSystemAccount задано как false.

Account

С помощью этого свойства можно определить, будет ли служба выполняться с системной учетной записью.

HelpText

Свойство только для чтения, которое возвращает справочный текст для задания имени пользователя и пароля.

ServiceInstaller является классом, необходимым для каждой службы. Он имеет свойства, уникальные для каждой службы внутри процесса: StartType, DisplayName, ServiceName и ServiceDependedOn:

Свойства ServiceInstaller

StartType

Указывает, запускается ли служба автоматически или вручную. Возможные значения: ServiceStartMode.Automatic, ServiceStartMode.Manual, ServiceStartMode.Disabled.

DisplayName

Является именем службы, которое выводится пользователю. Это имя используется также многими утилитами управления для контроля и мониторинга службы.

ServiceName

Является именем службы. Это значение должно быть идентично свойству ServiceName класса ServiceBase в программе службы.

ServicesDependentOn

Определяет массив служб, которые должны запускаться, прежде чем можно будет запустить эту службу. Когда служба запускается, все подчиненные службы запускаются автоматически.

Заметьте, что если изменяется имя службы в классе, производном от ServiceBase, то также необходимо изменить свойство ServiceName в объекта ServiceInstaller.

Во время тестирования задавайте StartType как Manual (вручную). Если остановка службы откажет, этот процесс нельзя уничтожить, так как он будет сконфигурирован для выполнения в контексте учетной записи System. Эту конфигурацию можно будет изменить позднее, когда все будет работать правильно.

ServiceInstallerDialog

Другим классом установки в пространстве имен System.ServiceProcess.Design является ServiceInstallerDialog. Если желательно, чтобы системный администратор вводил имя пользователя и пароль во время установки, может использоваться этот класс.

Если задать свойства Username и Password класса ServiceProcessInstaller как null, это диалоговое окно будет автоматически выводиться во время установки. Также можно в это время отменить установку:

InstallUtil

После добавления в проект классов для установки можно воспользоваться утилитой installutil.exe для установки и удаления cлужбы. Ввод командной строки для этих действий выглядит соответственно:

installutil quoteservice.exe

installutil /u quoteservice.exe

Если установка отказывает, проверьте файлы регистрации установки InstallUtil.InstallLog и <имя_службы>.InstallLog. Там можно найти очень полезную информацию, такую как "Указанная служба уже существует".

Клиент

После успешной установки службы и запуска ее вручную (см. следующий раздел для дальнейших указаний) можно запустить клиент, используя службы ММС со следующими настройками.

Мониторинг и управление службой

Для мониторинга и управления службой имеется несколько утилит. Они относятся к службам консоли ММС, которая, в свою очередь, является частью административной утилиты управления компьютером. Для каждой оконной системы мы получаем также утилиту командной строки net.exe, позволяющую управлять службами, sc.exe служит дополнительной утилитой командной строки, которая имеет значительно больше функций, чем команда net.exe, являющаяся частью Platform SDK. Мы создадим небольшое приложение Windows, использующее класс System.ServiceProcess.ServiceController для мониторинга и управления службами.

Консоль управления Microsoft (ММС)

Используя подключаемый модуль (snap-in) Services из консоли управления Microsoft (ММС), можно увидеть статус всех служб. Также можно послать службам управляющие запросы для останова, включения, выключения и изменения конфигурации. Подключаемый модуль Services является служебной управляющей, а также служебной конфигурационной программой:

Двойной щелчок на QuoteService открывает следующее диалоговое окно. Мы видим имя службы, описание, путь доступа к исполняемому файлу, тип запуска и статус. Служба в данный момент запущена. С помощью вкладки Log On в этом диалоговом окне можно изменить учетную запись для процесса службы.

net.exe

Подключаемый модуль Services использовать легко, но системный администратор не может его автоматизировать, так как его нельзя применить внутри административного сценария. Системный администратор может написать программу для Windows Scripting Host, чтобы облегчить свою повседневную работу. Для этой задачи существует утилита командной строки net.exe, имеющаяся в любой установленной системе Windows. Эта утилита используется для управления службами, net start показывает все выполняющиеся службы, net start имя_службы запускает службу, net stop имя_службы посылает службе запрос останова. Можно также временно остановить и продолжить работу службы с помощью net pause и net continue (конечно, если служба это допускает).

Результаты net start показаны в консольном окне:

sc.exe

Существует малоизвестная утилита sc.exe, которая является частью Microsoft Platform SDK. Необходимо установить Microsoft Platform SDK, чтобы получить доступ к этой утилите. Microsoft Platform SDK не является частью компакт-диска Visual Studio.NET. Этот дополнительный компакт-диск — часть MSDN, его можно загрузить из Интернета для подписчиков MSDN. Обычный путь доступа для установки такой утилиты — с:\Program Files\Microsoft Platform SDK\Bin\WinNT.

sc.exe — хорошая утилита для работы со службами. С ней можно сделать значительно больше по сравнению с утилитой net.exe. С помощью sc проверяется реальный статус, конфигурируются, удаляются и добавляются службы. Эта утилита помогает в том случае, когда удаление службы не может производиться обычным образом.

Server Explorer

Управлять службами можно также с помощью Server Explorer из Visual Studio.NET. Если вы не находите Server Explorer в текущей конфигурации, можно сделать его видимым с помощью меню View|Server Explorer. Выбирая службу и открывая контекстное меню, запускают и останавливают службу. Это контекстное меню также используется для добавления в проект класса ServiceController. Если вы желаете управлять специфической службой в приложении, перетащите службу из Server Explorer в конструктор — экземпляр ServiceController добавится в приложение. Свойства этого объекта автоматически задаются для доступа к выбранной службе и создается ссылка на System.ServiceProcess.dll. Можно использовать этот экземпляр для управления службой таким же образом, как мы это делаем в следующем разделе в базовом приложении для управления всеми службами.

Класс ServiceController

Создадим небольшое оконное приложение с помощью класса ServiceController для мониторинга и управления оконными службами.

Интерфейс пользователя для этого приложения имеет окно списка для вывода всех служб, четыре текстовых поля для вывода внешнего имени, статуса, типа и имени службы и четыре кнопки для отправки управляющих сообщений.

Здесь используется класс System.ServiceProcess.ServiceController, поэтому необходимо иметь ссылку на System.ServiceProcess.dll.

Мы реализуем метод RefreshServiceList(), который вызывается в конструкторе класса ServiceControlForm. Этот метод заполняет окно списка внешними именами всех служб. GetServices() является статическим методом класса ServiceController, и он возвращает массив ServiceController, представляющий все службы Windows. Класс ServiceController также имеет статический метод GetDevice(), который возвращает массив ServiceController, представляющий все драйверы устройств.

Окно списка заполняется с помощью связывания данных:

private System.ServiceProcess.ServiceController[] services;

public ServiceControlForm() {

//

// Требуется для поддержки Windows Form Designer

//

InitializeComponent();

RefreshServiceList();

}

protected void RefreshServiceList() {

services = ServiceController.GetServices();

listBoxServices.DisplayMember = "DisplayName";

listBoxServices.DataSource = services;

}

Теперь все службы Windows выводятся в окне списка и можно получить данные о каждой службе. Класс ServiceController имеет следующие свойства для данных о службе:

Свойства ServiceController

CanPauseAndContinue

Если службе можно послать запрос pause и continue, то возвращается true.

CanShutdown

true, если служба имеет программу обработки для выключения системы.

CanStop

true, если службу можно остановить.

DependentServices

Возвращает совокупность подчиненных служб. Если служба остановлена, то все подчиненные службы заранее останавливаются.

ServicesDependentOn

Возвращаем совокупность служб, которые зависят от этой службы.

DisplayName

Имя, которое должно выводиться для этой службы.

MachineName

Имя машины, на которой выполняется эта служба.

ServiceName

Имя службы.

ServiceType

Служба может выполняться внутри общего процесса, где более одной службы используют один и тот же процесс (Win32ShareProcess), или выполняться так, что существует только одна служба внутри процесса (Win32OwnProcess). Если служба может взаимодействовать с рабочим столом компьютера, то тип будет InteractiveProcess.

Status

Статус службы. Статус может быть running, stopped paused или в некотором промежуточном режиме, таком как start pending, stop pending и т.д.

В рассматриваемом приложении используются свойства DisplayName, ServiceName, ServiceType и Status для вывода данных о службе, а также CanPauseAndContinue и CanStop для включения и отключения кнопок Pause, Continue и Stop.

Метод OnSelectedIndexChanged() является методом обработки для окна списка. Он вызывается, когда пользователь выбирает службу в окне списка. В методе OnSelectedIndexChanged() внешнее имя и имя свойства задаются непосредственно с помощью свойств класса ServiceController. Статус и тип не могут просто задаваться, так как должна выводиться строка вместо числа, которое возвращает класс ServiceController. Метод SetServiceStatus() является вспомогательной функцией, просматривающей перечисление свойств Status для выводa строки статуса, а также включает и отключает кнопки. GetServiceTypeName() создает имя типа службы. ServiceType мы получаем из ServiceController.ServiceType представляет множество флажков, которые могут комбинироваться с помощью побитового оператора ИЛИ. Бит InteractiveProcess может задаваться вместе с Win32OwnProcess и Win32ShareProcess. Необходимо проверить, задан ли бит InteractiveProcess прежде чем переходить к проверке других значений:

protected string GetServiceTypeName(ServiceType type) {

string serviceType = "";

if ((type & ServiceType.InteractiveProcess) != 0) {

serviceType = "Interactive ";

type -= ServiceType.InteractiveProcess;

}

switch (type) {

case ServiceType.Adapter:

serviceType -= "Adapter";

break;

case ServiceType.FileSystemDriver:

case ServiceType.KernelDriver:

case ServiceType.RecognizerDriver:

ServiceType += "Driver";

break;

case ServiceType.Win32OwnProcess:

ServiceType += "Win32 Service Process";

break;

case ServiceType.Win32ShareProcess;

ServiceType += "Win32 Shared Process";

break;

default:

ServiceType += "unknown type " + type.ToString();

break;

}

return ServiceType;

}

protected void SetServiceStatus(ServiceController controller) {

buttonStart.Enabled = true;

buttonStop.Enabled = true;

buttonPause.Enabled = true;

buttonContinue.Enabled = true;

if (!controller.CanPauseAndContinue) {

buttonPause.Enabled = false;

buttonContinue.Enabled = false;

}

if (!controller.CanStop) {

buttonStop.Enabled = false;

}

ServiceControllerStatus status = controller.Status;

switch (status) {

case ServiceControllerStatus.ContinuePending:

textBoxServiceStatus.Text = "Continue Pending";

buttonContinue.Enabled = false;

break;

case ServiceControllerStatus.Paused;

textBoxServiceStatus.Text = "Paused";

buttonPause.Enabled = false;

buttonStart.Enabled = false;

break;

case ServiceControllerStatus.PausePending:

textBoxServiceStatus.Text = "Pause Pending";

buttonPause.Enabled = false;

buttonStart.Enabled = false;

break;

case ServiceControllerStatus.StartPending:

textBoxServiceStatus.Text = "Start Pending";

buttonStart.Enabled = false;

break;

case ServiceControllerStatus.Running:

textBoxServiceStatus.Text = "Running";

buttonStart.Enabled = false;

buttonContinue.Enabled = false;

break;

case ServiceControllerStatus.Stopped:

textBoxServiceStatus.Text = "Stopped";

buttonStop.Enabled = false;

break;

case ServiceControllerStatus.StopPending:

textBoxServiceStatus.Text = "StopPending";

buttonStop.Enabled = false;

break;

default:

textBoxServiceStatus.Text = "Unknown status";

break;

}

}

protected void OnSelectedIndexChanged(object sender, System.EventArgs e) {

ServiceController controller =

(ServiceController)listBoxServices.SelectedItem;

textBoxDisplayName.Text = controllerDisplayName;

textBoxServiceType.Text =

GetServiceTypeName(controller.ServiceType);

textBoxServiceName.Text = controller.ServiceName;

SetServiceStatus(controller);

}

Управление службой

С помощью класса ServiceController можно также посылать службе управляющие запросы.

Методы ServiceController

Start()

Start() сообщает SCM, что служба должна быть запущена. В нашей служебной программе вызывается OnStart().

Stop()

Stop() вызывает OnStop() в нашей служебной программе с помощью SCM, если свойство CanStop задано как true в классе службы

Pause()

Pause() вызывает OnPause(), если свойство CanPauseAndContinue задано как true.

Continue()

Continue() вызывает OnContinue(), если свойство CanPauseAndContinue задано как true.

ExecuteCommand()

С помощью ExecuteCommand можно послать службе специальную команду.

Код для управления службой следует далее. Так как код для запуска, останова, приостановки и временной остановки аналогичен, то используется только одна программа обработки для четырех кнопок:

protected void buttonCommand_Click(object sender, System.EventArgs e) {

Cursor Current = Cursors.WaitCursor;

ServiceController controller =

(ServiceController)listBoxServices.SelectedItem;

if (sender == this.buttonStart) {

controller.Start();

controller.WaitForStatus(ServiceControllerStatus.Running);

}  else if (sender == this.buttonStop) {

controller.Stop();

controller.WaitForStatus(ServiceControllerStatus.Stopped);

} else if (sender == this.buttonPause) {

controller.Pause();

controller.WaitForStatus(ServiceControllerStatus.Paused);

} else if (sender == this.buttonContinue) {

controller.Continue();

controller.WaitForStatus(ServiceControllerStatus.Running);

}

int index = listBoxService.SelectedIndex;

RefreshServiceList();

listBoxServices.SelectedIndex = index;

Cursor.Current = Cursors.Default;

}

protected void buttonExit_Click(object sender, System.EventArgs e) {

Application.Exit();

}

protected void buttonRefresh_Click(object sender, System.EventArgs e) {

RefreshServiceList();

}

Это действие может потребовать некоторого времени, поэтому курсор в первой инструкции переключается в курсор ожидания. С помощью метода WaitForStatus() мы ожидаем максимум только 10 с, пока служба изменит статус на запрошенное значение. После этого времени информация в окне списка обновляется, и выбирается та же служба, чтобы выводился новый статус этой службы.

Выполняющееся приложение выглядит так:

Поиск неисправностей

Поиск неисправностей работает для служб иначе, чем для обычных приложений. Лучший способ образовать службу — создание сначала требуемой функциональности и тестового клиента. В этом случае выполняется нормальная отладка и обработка ошибок. Как только приложение запустится, можно начинать создавать службу, используя эту сборку. Конечно, со службой по-прежнему возможны проблемы:

□ В службе не выводите ошибки в окне сообщений (за исключением интерактивных служб, которые выполняются на системе клиента). Вместо этого для записи ошибок используйте службу регистрации событий. Конечно, можно вывести окно сообщений для информирования пользователя об ошибках в клиентском приложении, использующем службу.

□ Службу нельзя запустить из отладчика, но отладчик можно присоединить к выполняющемуся процессу службы. Откройте исходный код службы и задайте точки прерывания. В меню Visual Studio.NET Debug выберите Processes и присоедините выполняющийся процесс службы.

□ Для мониторинга активности служб можно использовать монитор производительности. Добавьте к службе свои собственные объекты производительности, это даст некоторую полезную информацию для отладки. Можно задать объект для указания общего числа отправленных цитат, время, которое необходимо для инициализации и т.д.

Интерактивные службы

Если служба предназначена для выполнения на клиентской системе, полезно выводить окна сообщений пользователю. Если служба должна выполняться на сервере, который будет заперт в компьютерном зале, служба никогда не должна выводить окно сообщений. Когда открытое окно сообщений ожидает некоторого ввода пользователя, то такой ввод, возможно, не произойдет в течение нескольких дней, так как никто не проверяет сервер в компьютерном зале; но все может оказаться даже хуже — если служба не сконфигурирована как интерактивная служба, окно сообщений открывается на другой, скрытой, оконной станции. В таком случае никто не сможет ответить на это окно сообщений, и служба будет заблокирована.

Никогда не открывайте диалоговые окна для служб, выполняющихся на серверной системе. Никто не ответит на это диалоговое окно.

В тех случаях, где действительно желательно взаимодействие с пользователем, можно сконфигурировать интерактивную службу. Некоторыми примерами таких интерактивных служи являются Print Spooler, который выводит для пользователя сообщения на бумаге, и служба NetMeeting Remote Desktop Sharing.

Чтобы сконфигурировать интерактивную службу, необходимо задать функцию Allow service to interact with desktop (Разрешить службе взаимодействовать с рабочим столом) в Computer Management. Это изменяет тип службы, добавляя к типу флажок SERVICE_INTERACTIVE_PROCESS.

Регистрация событий

Службы могут сообщать об ошибках и другую информацию, отмечая ее в журнале событий. Служебный класс, производный из ServiceBase, автоматически регистрирует события, когда свойство AutoLog задано как true. Класс ServiceBase проверяет это свойство и заносит запись в журнал при событиях запуска, остановки, паузы и продолжения. Вот пример журнальной записи, сделанной службой.

Для регистрации специальных событий можно использовать классы из пространства имен System.Diagnostics.

Архитектура регистрации событий

По умолчанию Event Log (Журнал событий) хранится в трех файлах журналов: Application, Security и System. Просматривая конфигурацию реестра службы регистрации событий, можно увидеть три записи в HKLM\System\CurrentControlSet\Services\EventLog с конфигурациями, указывающими на определенные файлы. Файл журнала System используется из драйверов системы и устройств, приложения и службы записывают в журнал Application. Security является журналом только для чтения приложений. Свойство аудита операционной системы использует журнал Security.

Можно прочитать эти события с помощью административной утилиты Event Viewer. Event Viewer запускается непосредственно из Server Explorer, входящего в Visual Studio.NET. Сделайте щелчок правой кнопкой мыши на пункте Event Logs и выберите запись Launch Event Viewer из контекстного меню:

В журнале событий будет помещена следующая информация:

□ Type может быть Information, Warning и Error. Information — это редкая успешная операция, Warning — проблема, которая не является немедленно значимой, и Error — основная проблема. Дополнительными типами являются FailureAudit и SuccessAudit, но эти типы используются только для журнала Security.

□ Date и Time показывают время, когда происходит событие.

□ Source — имя программного обеспечения, регистрирующего событие. Source для журнала Application конфигурируется в HKLM\System\CurrentControlSet\Services\EventLog\Application. Под этим ключом конфигурируется значение EventMessageFile для указания на DLL ресурса, который содержит сообщения об ошибках.

□ Category можно определить так, чтобы журналы событий фильтровались при использовании Event View.

□ Идентификатор события определяет сообщение об определенном событии.

Классы регистрации событий

Пространство имен System.Diagnostics имеет несколько классов для регистрации событий:

□ С помощью класса EventLog можно прочитать и внести записи в журнал событий, а также определить приложения как источники событий.

□ EventLogEntry является единственным входом в журнал событий. С помощью EventLogEntryCollection можно просмотреть EventLogEntry.

□ Класс EventLogInstaller предназначен для установки компонента EventLog. EventLogInstaller вызывает EventLog.CreateEventSource() для создания источника событий.

□ С помощью EventLogTraceListener можно записать в журнал событий трассировки. Этот класс реализует абстрактный класс TraceListener.

Добавление регистрации событий

Если свойство AutoLog класса ServiceBase задано как true, то автоматически включается регистрация событий. Класс ServiceBase регистрирует информационное событие при запросах службы для запуска, остановки, паузы и продолжения. В классе ServiceInstaller создается экземпляр EventLogInstaller, чтобы сконфигурировать источник журнала событий. Этот источник журнала событий имеет такое же имя, как и служба. Для записи события используем статический метод WriteEntry() класса EventLog. Свойство Source было уже задано в классе ServiceBase:

EventLog.WriteEntry("event log message");

Этот метод регистрирует информационное событие. Если должно быть создано событие предупреждения или ошибки, то для определения этого типа используется перезагруженный метод WriteEvent():

EventLog.WriteEntry("event log message", EventLogEntryType.Warning);

EventLog.WriteEntry("event log message", EventLogEntryType.Error);

Добавление регистрации событий в другие типы приложений

Для служб класс ServiceBase автоматически добавляет свойства регистрации событий. Если желательно использовать регистрацию событий в других типах приложений, это легко делается с помощью Visual Studio.NET.

□ Используйте ToolBox для добавления компонента EventLog в конструктор.

□ Задайте свойство Log компонента EventLog как Application, а свойство Source как выбранное имя. Обычно это бывает имя приложения, которое показано в Event View.

□ Теперь можно записать журналы с помощью метода WriteEntry() экземпляра EventLog.

□ Можно добавить программу установки из пункта контекстного меню Add Installer компонента EventLog. Это создает класс ProjectInstaller, который конфигурирует источник событий в реестре.

□ С помощью команды installutil теперь можно зафиксировать приложение, installutil вызывает класс ProjectInstaller и регистрирует источник событий.

Для установки типа хсору последние два шага на самом деле не нужны. Если задано свойство Source экземпляра EventLog, источник автоматически регистрируется, когда журнал событий заполняется в первый раз. Это действительно легко сделать, но для реального приложения предпочтительнее добавить программу установки: с помощью installutil /u конфигурация регистрации событий отменяется. Если приложение просто удаляется, этот ключ реестра остается, если не будет вызван метод EventLog.DeleteEventSource().

Трассировка

Можно сделать так, чтобы все сообщения трассировки направлялись в журнал событий. На самом деле это не нужно, так как в нормально работающей системе журнал событий будет перегружен сообщениями трассировки. Системный администратор пропустит действительно важные записи, если это произойдет. Помните, что тип записи события — это ошибка, предупреждение и информационное. При этом информационные сообщения редко являются информацией об успехе. Включение трассировочных сообщений в журнал событий может быть полезным свойством для тестирования проблемных служб. Трассировка возможна как с отладочным, так и с окончательным кодом.

Чтобы послать трассировочные сообщения в журнал событий, должен быть создан объект EventLogTraceListener и добавлен в список приемника класса Trace:

EventLogTraceListener listener = new EventLogTraceListener(eventLog1);

Trace.Listeners.Add(listener);

Теперь все трассировочные сообщения посылаются в журнал событий:

Trace.WriteLine("trace message");

Дополнительная информация о методах трассировки находится в главе 6.

Создание приемника событий

Теперь было бы полезно создать приложение, которое получает событие, когда в службе происходит что-то плохое. Мы создадим простое оконное приложение, отслеживающее события службы Quote:

Оконное приложение имеет только окно списка и кнопку выхода:

Компонент EventLog добавляется в этот проект перетаскиванием его из панели инструментов. Свойство Log задается как Application, a Source как источник службы QuoteService. Класс EventLog также имеет свойство EnableRaisingEvents. До сих пор мы не говорили об этом свойстве. По умолчанию для него используется значение false, задание его как true означает, что событие создается каждый раз, когда происходит это событие, и можно написать обработчик событий для оконного события EntryWritten.

В файле EventListener.cs свойства задаются в методе InitializeComponent():

private void InitializeComponent() {

this.eventLogQuote = new System.Diagnostics.EventLog();

this.buttonExit = new System.Windows.Forms.Button();

this.listBoxEvents = new System.Windows.Forms.ListBox();

((System.ComponentModel.ISupportInitialize)

(this.eventLogQuote)).BeginInit();

this.SuspendLayout();

//

// eventLogQuote

//

this.eventLogQuote.EnableRaisingEvents = true;

this.eventLogQuote.Log = "Application";

this.eventLogQuote.Source = "QuoteService";

this.eventLogQuote.SynchronizingObject = this;

this.eventLogQuote.EntryWritten +=

new System.Diagnostics.EntryWrittenEventHandler(this.OnEntryWritten);

// ...

Программа обработки OnEntryWritten() получает объект EntryWrittenEventArgs в качестве аргумента, где можно получить всю информацию из события. С помощью свойства Entry мы получаем объект EventLogEntry с информацией о времени, источнике события, типе, категории и т. д.:

protected void OnEntryWritten(object sender, System.Diagnostics.EntryWrittenEventArgs e) {

DateTime time = e.Entry.TimeGenerated;

string message = e.Entry.Message;

listBoxEvents.Items.Add(time + " " + message);

}

Выполняющееся приложение показывает все события для QuoteService:

Мониторинг производительности

Мониторинг производительности может использоваться для получения информации о нормальном выполнении службы. Это прекрасный инструмент, который помогает понять нагрузку системы и наблюдать изменения и тенденции.

Windows 2000 имеет множество объектов производительности, таких как System, Memory, Objects, Process, Processor, Thread, Cache и т. д. Каждый из этих объектов имеет множество показателей для мониторинга. С помощью объекта Process для всех процессов или для определенных экземпляров процессов можно контролировать время пользователя, счетчик дескрипторов. Ошибки страниц, счетчик потоков выполнения и т. д. В некоторых приложениях также имеются специфические объекты, например SQL Server.

Для нашей службы цитат может представлять интерес получение информации о числе клиентских запросов, размере данных, посылаемых по каналам связи, и т.д.

Классы мониторинга производительности

Пространство имен System.Diagnostics имеет следующие классы для мониторинга производительности:

□ PerformanceCounter используется как для мониторинга счетчиков, так и для записи счетчиков. С помощью этого класса можно создавать новые категории производительности.

□ С помощью класса PerformanceCounterCategory можно пройти через все существующие категории, а также создать новые. Программным путем получаются все счетчики категории.

□ Класс PerformanceCounterInstaller используется для установки счетчиков производительности, аналогично классу EventLogInstaller, о котором упоминалось ранее.

Построитель счетчиков производительности

Можно создать новую категорию, выбирая счетчики производительности в Server Explorer. Категория называется Quote Service. В таблице показаны все счетчики производительности нашей службы:

Имя

Описание

Тип

# of Bytes sent

Общее число байтов, посланных клиенту.

NumberOfItems32

# of Bytes sent/sec

Число байтов, посылаемых клиенту в одну секунду.

NumberOfItems32

# of Requests

Общее число запросов.

NumberOfItems32

# of Requests /sec

Число запросов в одну секунду.

NumberOfItems32

Построитель счетчика производительности записывает конфигурацию в базу данных производительности. Это может также делаться динамически с помощью метода Create() класса PerformanceCategory в пространстве имен System.Diagnostics. Программу установки для других систем можно легко добавить в последующем с помощью Visual Studio.NET.

Построитель счетчика производительности запускается из Server Explorer при выборе контекстного меню Performance Counters|Create New Category:

Добавление счетчиков производительности

Теперь мы хотим добавить счетчики производительности в сервер цитат. Класс QuoteServiсе не располагает информацией, необходимой для счетчиков производительности. Мы хотим получить число запросов, но после запуска службы QuoteService не получает запросов. Информация полностью содержится в классе QuoteServer, созданном ранее.

Добавление поддержки Visual Studio.NET Designer в библиотеку классов

Можно вручную добавить в код экземпляры класса PerformanceCounter либо использовать приложение Visual Studio.NET Designer. С его помощью перетаскиваются компоненты PerformanceCounter из панели инструментов на его рабочую поверхность. Поддержку легко добавить в библиотеку компонентов, выводя класс из System.ComponentModel.Component. Метод InitializeComponent(), который используется для задания свойств компонентов, будет исполняться автоматически, необходимо добавить лишь его вызов.

Добавление компонентов PerformanceCounter

Далее можно добавить компоненты PerformanceCounter из панели инструментов. Для нашей службы добавляется четыре экземпляра, где свойство CategoryName задается как Quote Service Count для всех объектов, а свойство CounterName задается одним из значений, доступным в выбранной категории. Свойство ReadOnly должно быть задано как False.

Код, который был внесен в InitializeComponent() путем добавления Components в конструктор и заданием свойств, выглядит так:

private void InitializeComponent() {

// ...

//

// performanceCounterRequestsPerSec

//

this.performanceCounterRequestsPerSec.CategoryName =

"Quote Service Counts";

this.performanceCounterRequestsPerSec.CounterName =

"# of Requests / sec";

this.performanceCounterRequestsPerSec.ReadOnly = false;

//

// performanceCounterBytesSentTotal

//

this.performanceCounterBytesSentTotal.CategoryName =

"Quote Service Counts";

this.performanceCounterBytesSentTotal.CounterName =

"# of Bytes sent";

this.performanceCounterBytesSentTotal.ReadOnly = false;

//

// performanceCounterBytesSentPerSec

//

this.performanceCounterBytesSentPerSec.CategoryName =

"Quote Service Counts";

this.performanceCounterBytesSentPerSec.CounterName =

"# of Bytes sent / sec";

this.performanceCounterBytesSentPerSec.ReadOnly = false;

//

// performanceCounterRequestsTotal

//

this.performanceCounterRequestsTotal.CategoryName =

"Quote Service Counts";

this.performanceCounterRequestsTotal.CounterName =

"# of Requests";

this.performanceCounterRequestsTotal.Readonly = false;

// ...

Счетчики производительности, которые показывают общие значения, увеличиваются в методе Listener() класса QuoteServer. Метод Increment() увеличивает счетчик на 1, метод IncrementBy() увеличивает счетчик на значение аргумента.

Для счетчиков производительности, которые показывают посекундные значения, в методе Listener() обновляются только две переменные — requestPerSec и bytessPerSec:

void protected void Listener() {

try {

listener = new TCPListener(port);

listener.Start();

while (true) {

Socket socket = listener.Accept();

if (socket == null) {

return;

}

string message = GetRandomQuoteOfTheDay();

UnicodeEncoding encoder = new UnicodeEncoding();

byte [] buffer = encoder.GetBytes(message);

socket.Send(buffer, buffer.Length, 0);

socket.Close();

performanceCounterRequestsTotal.Increment();

performanceCounterBytesSentTotal.IncrementBy(nBytes);

requestsPerSec++;

bytesPerSec += Bytes;

}

} catch (Exception e) {

string message = "Quote Server failed in Listener: " + e.Message;

eventLog.WriteEntry(message, EventLogEntryType.Error);

}

}

Чтобы показывать обновленные значения каждую секунду, используется компонент Timer. Метод OnTimer() вызывается раз в секунду и задает счетчики производительности с помощью свойства RawValue класса PerformanceCounter:

protected void OnTimer(object sender, system.EventArgs e) {

performanceCounterBytesSentPerSec.RawValue = bytesPerSec;

performanceCounterRequestsPerSec.RawValue = reguestsPerSec;

bytesPerSec = 0;

requestsPerSec = 0;

}

perfmon.exe

Теперь можно контролировать нашу службу. Утилита Performance может запускаться из Administrative Tools|Performance. Нажимая кнопку + в панели инструментов, можно добавить счетчики производительности. Quote Service будет определяться как объект производительности. Все сконфигурированные счетчики показаны в списке счетчиков:

После добавления счетчиков можно увидеть счетчики нашей службы в ходе времени. Используя эту утилиту производительности, можно также создать файлы журналов для последующего анализа производительности.

Служба счетчика производительности

Если не существует ссылки на объект счетчика производительности, и объект был удален сборщиком мусора, счетчик этого объекта теряется и запускается в следующий раз с 0. Чтобы решить эту проблему, состояние можно хранить в службе счетчиков производительности. С помощью .NET Framework служба счетчиков производительности устанавливается в системе. Необходимо только запустить службу; можно также сконфигурировать службу для автоматического запуска во время начальной загрузки системы.

Свойства служб Windows 2000

Windows 95, 98 и ME не поддерживают службы Windows. Они поддерживаются в Windows NT, Windows 2000 и Windows ХР. Windows 2000 имеет несколько больше свойств для служб по сравнению с Windows NT. Рассмотрим свойства служб в Windows 2000.

Изменения сетевого соединения и события электропитания

В Windows 2000 не требуется, чтобы система перезагружалась так часто, как это было необходимо в Windows NT; например, не нужно перезагружать систему, когда изменяется адрес IP, — служба получает события при смене адреса и действует соответственно. Windows 2000 посылает следующие управляющие коды службам, когда изменяется сетевое соединение:

Управляющий код

SERVICE_CONTROL_NETBINDADD

Доступен новый компонент для соединения.

SERVICE_CONTROL_NETBINDREMOVE

Компонент для соединения был удален. Необходимо заново считать информацию соединения и отсоединиться от удаленного компонента.

SERVICE_CONTROL_NFTBINDENABLED

Ранее отключенное соединение снова включено.

SERVICE_CONTROL_NETBINDDISABLE

Ранее включенное соединение теперь отключено.

Если служба использует соединение, необходимо заново прочитать информацию соединения и удалить соединения, которые стали недоступными. Служба реагирует на сетевые изменения, поэтому перезагрузка не требуется.

Windows 2000 добавляет также увеличенную поддержку управления электропитанием. Существует поддержка для перевода системы в нерабочее состояние — память записывается на диск, поэтому возможна более быстрая начальная загрузка системы. Также возможно временно остановить машину, чтобы сократить потребление электроэнергии, при этом система в случае необходимости автоматически пробуждается.

Для всех событий электропитания служба получает управляющий код SERVICE_CONTROL_POWEREVENT с дополнительными параметрами. В параметре отражена причина события. Код причины может говорить о разряженности батареи, о том, что система переходит в приостановленное состояние, или об изменении статуса электропитания. В зависимости от кода причины служба должна замедлить скорость, приостановить фоновые потоки выполнения, закрыть сетевые соединения, закрыть файлы и т.д.

Классы в пространстве имен System.ServiceProcess также имеют поддержку для этих свойств Windows 2000 Служба конфигурируется так, чтобы она реагировала на события паузы и продолжении с помощью свойства CanPauseAndContinue, и задается свойство для управления электропитанием: CanHandlePowerEvent. Службы Windows 2000, которые управляют электропитанием, регистрируются в SCM с помощью метода API Win32 RegisterServiceCtrlHandlerEx().

Задавая значение CanHandlePowerEvent как True, метод

protected virtual bool OnPowerEvent(PowerBroadcastStatus power Status);

будет вызываться, как только изменится статус электропитания. Некоторые из значений, получаемые из PowerBroadcastStatus, перечислены ниже в таблице:

Значение powerStatus

Описание

BatteryLow

Слабый заряд батареи. Необходимо сократить функционирование службы до минимума.

PowerStatusChange

Произошло переключение электропитания от батареи на внешний источник, или мощность батареи опустилась ниже допустимого значения и т.д.

QuerySuspend

Полномочия системных запросов перешли в приостановленный режим. Можно отказаться от полномочий или приготовиться к переходу в приостановленный режим, закрывая файлы, разъединяя сетевые соединения и т.д.

QuerySuspendFailed

Переход в приостановленный режим был отвергнут системой. Можно продолжать с той же функциональностью.

Suspend

Никто не отменил запрос перехода в приостановленный режим. Система скоро будет приостановлена.

Восстановление

Автоматическое восстановление является вопросом конфигурации, оно используется для всех свойств выполняющихся в системе Windows 2000. Если процесс службы разрушается, то служба автоматически запускается снова или конфигурируется специальный файл, или автоматически перезагружается вся система. Обычно существует причина разрушения службы, и нежелательно автоматически непрерывно перезагружать систему, поэтому нужно разнообразить ответы на первую, вторую и последующие ошибки.

Приложения COM+ в роли служб

Начиная с Windows ХР (кодовое имя Whistler), приложение COM+ выполняется как служба. В Windows ХР служба имеет прямой доступ к таким службам COM+, как транзакции, пулы объектов, пулы потоков выполнения и т.д. Если желательно использовать службы COM+ в Windows 2000 как службы Windows, то создаются два отдельных приложения: одно имеет дело с функциями службы, а второе — со службами COM+. Это нам дает некоторые преимущества:

□ Легче создать служебное приложение. Нам не нужно больше иметь дело со специальной установкой службы, так как это выполняется прямо из конфигурации COM+.

□ Приложение COM+ может действовать как служба. Оно автоматически запускается во время начальной загрузки, имеет права учетной записи System и реагирует на управляющие коды службы, которые посылаются из управляющей программы службы.

□ Служебное приложение, создаваемое как приложение COM+, имеет прямой доступ к таким службам COM+, как управление транзакциями, пулы объектов, пулы потоков выполнения и т.д.

Больше о службах COM+ можно прочитать в главе 20.

Заключение

В этой главе было показано, что такое службы Windows и как они создаются с помощью .NET Framework. Приложения в службах Windows запускаются автоматически во время начальной загрузки, и мы можем использовать привилегированную учетную запись System в качестве пользователя службы. .NET Framework обладает хорошей поддержкой служб. Весь вспомогательный код, который требуется для создания, управления и установки служб, находится в классах .NET Framework в пространстве имен System.ServiceProcess. С помощью классов System.Diagnostic становятся легко доступными все технологии, необходимые для таких служб, как регистрация событий и мониторинг производительности.

Глава 25

Система безопасности .NET

Вы садите за компьютером и работаете с приложением. За сценой приложение реагирует на тот факт, что вы пытаетесь использовать свойство, для которого у приложения нет подходящего модуля. Оно соединяется с Интернетом, загружает модуль в глобальный кэш сборок и начинает выполнение, и все это происходит без взаимодействия с пользователем.

Этот вид неявного обновления станет нормой в недалеком будущем, но очевидно, что здесь существуют проблемы, связанные с безопасностью так называемого мобильного кода. Какие имеются доказательства, что загруженный код надежен? Как узнать, что был получен именно тот модуль, который был запрошен? Что неявно делает CLR, чтобы гарантировать, например, что элемент управления на сайте Web не читает нашу почту?

Платформа .NET реализует политику системы безопасности в сборках. Она использует информацию о сборках, например, откуда они и кем опубликованы, чтобы разделить их на группы кодов с общими характеристиками. Например, весь код из локальной интранет помещается в единую группу. Он использует политику безопасности (обычно определяемую системным администратором с помощью утилиты caspol.exe или ММС) для назначения привилегий.

Политика безопасности .NET в сборках позволяет убедиться, что сборка исходит от того, кто ее опубликовал, и распределена между группами с одинаковыми характеристиками. Что необходимо сделать, чтобы инициировать систему безопасности для машины или для определенного приложения? Ничего — весь код автоматически выполняется в контексте безопасности CLR, хотя и возможно отключение системы безопасности по какой-либо причине.

В дополнение к уверенности в том, что выполняемый код надежен, также важно знать, что мы предоставляем пользователю приложения доступ к требуемым ему свойствам, но не более того. В эффективном управлении пользователями и их ролями может помочь система безопасности .NET на основе ролей.

В этой главе мы обсудим свойства управления безопасностью, доступные в .NET, включая защиту от злонамеренного кода, администрирование политиками безопасности и программный доступ к подсистеме безопасности. Мы также рассмотрим развертывание системы безопасности приложений .NET в коротких примерах приложений, которые закрепят концепции этой главы.

Система безопасности доступа к коду

Система безопасности доступа к коду является свойством .NET, которое управляет кодом в зависимости от нашего уровня доверия ему. Если система CLR достаточно доверяет коду, она начнет его выполнение. Однако в зависимости от полномочий, предоставленных сборкой, это может происходить в ограниченной среде. Если код недостаточно надежен для выполнения, или если он реализуется, но затем пытается выполнить действие, для которого не имеет соответствующих прав, порождается исключение безопасности (типа SecurityException или его подкласса). Система безопасности доступа к коду означает, что можно остановить выполнение злонамеренного кода или позволить коду выполняться в защищенной среде, где он не принесет никакого вреда.

Например, если пользователь пытается выполнить приложение, в котором помещен код, загруженный из Интернета, то используемая по умолчанию политика безопасности инициирует исключение и приложение невозможно будет запустить. Аналогичным образом, пользователь, работающий с приложением с сетевого диска, может начать его выполнение, но если приложение попытается обратиться к файлу на локальном диске, возникнет исключение и в зависимости от обработки ошибок в приложении оно либо аккуратно обработается, либо будет завершено.

Для большинства приложений система безопасности доступа к коду в .NET является существенным достоинством, но только одной из многих. Она предоставляет высокий уровень защиты от злонамеренного кода, но обычно нам не требуется в это вмешиваться. Особенно важно для нас, что одной из областей, в которую мы будем вовлечены, является управление политикой безопасности при конфигурации настольных компьютеров для надежного кода из приложения, поступившего от поставщиков программного обеспечения.

Другой областью, где система безопасности доступа к коду наиболее важна, является создание приложения, включающего элемент, безопасность которого необходимо тщательно контролировать. Например, если существует база данных в организации, содержащая крайне важные данные, необходимо использовать систему безопасности доступа к коду, чтобы можно было определять, какому коду разрешается доступ к базе данных, а какой код не может получить к ней доступ.

Важно понимать, что система безопасности доступа к коду предназначена для защиты ресурсов (локальных дисков, сети, интерфейса пользователя) от злонамеренного кода, но она не является инструментом для защиты программного обеспечения от пользователя. Для системы безопасности, связанной с пользователями, обычно используется встроенная в Windows 2000 подсистема безопасности пользователей или система безопасности .NET на основе ролей, которая будет рассмотрена позже.

Система безопасности доступа к коду основывается на двух высокоуровневых концепциях: Code Groups (Группы кодов) и Permissions (Полномочия).

□ Группы кодов (Code Groups) собирают вместе код, который имеет общие характеристики, хотя наиболее важным свойством является источник его происхождения. Например, группы кодов "Интернет" (источники кода из Интернета) и "интранет" (источники кода в LAN). Информация, используемая для помещения сборок в группы кода, называется свидетельством (evidance). Другое свидетельство, собираемое CLR, включает издателя кода, его устойчивое имя и (где применимо) URI, из которого он был загружен. Группы кода организуются в иерархию, и сборки почти всегда соответствуют нескольким группам кода. Группа кода в корне иерархии называется "All Code" и содержит все другие группы кода. Иерархия используется для определения, каким группам кода принадлежит сборка; если сборка не предоставляет свидетельство, которое соответствует определенной группе в дереве, не делается никаких попыток отнести ее к описанным ниже группам кода.

□ Полномочия (Permissions), или права, являются действиями, которые разрешается выполнить каждой группе кода. Например, полномочия включают "возможность обратиться к интерфейсу пользователя" и "возможность обратиться к локальной памяти". Системный администратор обычно управляет полномочиями, и это можно делать на уровне предприятия, уровне машины и уровне пользователя.

Виртуальная система выполнения внутри CLR загружает и реализует программы. Она предоставляет функции, необходимые для выполнения управляемого кода, и использует метаданные сборок для соединения в модули. Когда VES загружает сборку, она соответствует одной или нескольким группам кода. Каждой группе кода даются одно или несколько полномочий, которые определяют, какие действия могут делать сборки в этих группах кода. Например, если группе кода MyComputer присвоено полномочие FileIOPermission, то это означает, что сборки локальной машины могут читать и записывать в локальную файловую систему.

Группы кода

Группы кода имеют требование записи, называемое условием членства (Membership Condition). Чтобы сборка была внесена в группу кода, она должна соответствовать условию членства группы. Условия членства выглядят как запись "сборка с сайта www.microsoft.com" или "Издателем этого программного обеспечения является Microsoft Corporation".

Каждая группа кода имеет одно и только одно условие членства. Здесь представлены типы условий членства в группах кода, доступные в .NET:

□ Zone (Зона) — регион, из которого происходит код.

□ Site (Сайт) — сайт Web, из которого происходит код.

□ Strong name (Строгое имя) — уникальное, проверяемое имя кода. Часто называется 'общим именем'.

□ Publisher (Издатель) — издатель кода.

□ URL — определенное расположение, из которого происходит код.

□ Hash value (хэш-значение) — хэш-значение сборки.

□ Skip verification (Контроль пропуска) — код, который запрашивает сборку, обходит проверки контроля кода.

□ Application directory (Каталог приложения) — расположение сборки в приложении.

□ All code (Весь код) — весь код удовлетворяет этому условию.

□ Custom (Специальный) — определяемое пользователем условие.

Первым типом условия членства в списке является условие Zone, оно наиболее часто используется. В зоне определяется регион происхождения фрагмента кода: MyComputer, Intranet, Trusted и Untrusted. Эти регионы управляются с помощью Security Options в Internet Explorer, об этом мы узнаем больше в последующем при рассмотрении управления политиками системы безопасности. Хотя настройки управляются с помощью Internet Explorer, они применимы ко всей машине. Ясно, что эти конфигурационные параметры недоступны в браузерах других компаний и, фактически, внутристраничные элементы управления, написанные с помощью .NET Framework, не будут работать в браузерах, отличных от Internet Explorer.

Группы кодов организуются в иерархию с условием членства All Code в корне.

Можно видеть, что каждая группа кода имеет одно условие членства и обладает полномочиями, которые должны быть предоставлены группе кода. Мы рассмотрим полномочия более подробно позже. Отметим, что если сборка не соответствует условию членства в группе кода, CLR не пытается сопоставить ее с нижерасположенными группами кода.

Caspol.exe — утилита политики системы безопасности доступа к коду

К утилите командной строки политики системы безопасности доступа к коду мы часто прибегаем в этой главе. Она позволяет просматривать и управлять политикой безопасности. Чтобы получить список ее параметров, введите следующую команду:

caspol.exe

.NET содержит также подключаемый модуль (snap-in) для управляющей консоли Microsoft, обеспечивающий регулирование системы безопасности доступа к коду. Однако ограничимся утилитой командной строки, в этом случае будет легче следовать приведенным примерам и можно создавать сценарии для изменения политики системы безопасности, очень полезные применительно к большому числу машин.

Рассмотрим группы кода на машине с помощью утилиты caspol.exe. Вывод команды перечисляет иерархическую структуру групп кода, дающую описание каждой группы кода. Введите следующую команду:

caspol.exe -listdescription

Microsoft (R) .NET Framework CasPol 1.0.xxxx.xx

Copyright (c) Microsoft Corp 1999-2001. All rights reserved.

Security is ON

Execution checking is ON

Policy change prompt is ON

Level = Machine

Full Trust Assemblies:

1. All_Code: This code group grants no permissions and forms the root of the code group tree.

1.1. My_Computer_Zone: This code group grants full trust to all code originating on the local machine.

1.2. Local.Intranet_Zone: This code group grants the intranet permission set to code from the intranet zone. This permission set grants intranet code the right to use isolated storage, full UI access, some capability to do reflection and limited access to environment variables.

1.2.1. Intranet_Same_Site_Access: All intranet Code gets the right to connect back to the site of its origine.

1.2.2. Intranet_Same_Directory_Access: All intranet code gets the right to read from its install directory.

1.3. Internet_Zone: This code group grants code from the Internet zone the Internet permission set. This permission set grants Internet code the right the use isolated storage an a limited UI access.

1.3.1. Internet_Same_Site_Access: All Internet Code gets the right to connect back to the site of its origin.

1.4. Restricted_Zone: Code coming from a restricted zone does not receive any permissions.

1.5. Trusted_Zone: Code from a trusted zone is granted the Internet permission set. This permission set grants the right the use isolated storage and limited UI access.

1.5.1. Trusted_Same_Site_Access: All Trusted Code gets the right to connect back to the site of its origin.

1.6. Microsoft_Strong_Name: This code group grants code signed with the Microsoft strong name full trust.

1.7. Standards_Strong_Name: This code group grants code signed with the Standards strong name full trust.

Success

Подсистема безопасности .NET следит, чтобы коду из каждой группы разрешалось делать только определенные вещи. Например, код из зоны Интернета будет, по умолчанию, иметь значительно более строгие ограничения, чем код с локального диска. Коду с локального диска обычно предоставляется доступ к данным на локальном диске, но сборкам из Интернета по умолчанию это полномочие не предоставляется.

Используя утилиту caspol и ее эквивалент в панели управления Microsoft, можно определить, какой уровень доверия мы имеем для каждой группы доступа к коду, а для группы управляющего кода рассмотреть полномочия более детальным образом.

Давайте еще раз взглянем на группы доступа к коду, но в более обобщенном виде. Зарегистрируйтесь в системе как локальный администратор, откройте командную строку и введите команду:

caspol.exe -listgroups

Появится вывод:

Microsoft (R) .NET Framework CasPol 1.0.xxxx.x

Copyright (c) Microsoft Corp 1999-2001. All rights reserved.

Security is ON

Execution checking is ON

Policy change prompt is ON

Level = Machine

Code Groups:

1. All code: Nothing

1.1. Zone - My Computer: FullTrust

1.2. Zone - Intranet: FullTrust

1.2.1. All code: Same site Socket and Web.

1.2.2. All code: Same directory FileIO — Read, PathDiscovery

1.3. Zone — Internet: Internet

1.3.1. All code: Same site Socket and Web.

1.4. Zone — Untrusted: Nothing

1.5. Zone — Trusted: Internet

1.5.1. All code: Same site Socket and Web.

1.6. Strong Name -

00240000048000009400000006020000002400005253413100040000010001000

7D1FA57C4AED9F0A32E84AA0FAEFD0DE9E8FD6AEC8F7FB03766C834C999

21EB23BE79AD9D5DCC1DD9AD236132102900B723CF980957FC4E177

108FC607774F29E8320E92EA05ECE4E821C0A5EFE8F1645C4C0C93C1AB9

9285D622CAA652C1DFAD63D745D6F2DE5F17E5EAF0FC4963D261C8A124

36518206DC093344D5AD293:

FullTrust

1.7. StrongName - 00000000000000000400000000000000: FullTrust

Success

Можно заметить в начале вывода Security is ON. Позже в этой главе мы увидим, что это может быть выключено и снова включено.

Настройка ExecutionChecking задается включенной по умолчанию, это означает, что всем сборкам должно быть предоставлено разрешение выполнения, прежде чем они смогут выполниться. Если проверка выполнения выключена с помощью caspol (caspol.exe -execution on|off), то сборки, которые не имеют полномочий на выполнение, смогут реализоваться, хотя они способны вызвать исключения системы безопасности, если попытаются действовать вопреки политике безопасности в ходе этого выполнения.

Параметр Policy change prompt определяет, будет ли появляться предупреждающее сообщение "Are you sure" при попытке изменить политику безопасности.

Когда код разбит на группы, можно управлять системой безопасности на более детальном уровне и применять full trust (полное доверие) к значительно меньшей части кода. Отметим, что каждая группа имеет свою метку (такую, как "1.2"). Эти метки генерируются автоматически .NET и могут различаться на разных машинах. Обычно безопасность не контролируется для каждой сборки, она применяется на уровне группы кода.

Представляет интерес вопрос, как действует caspol.exe, когда машина имеет несколько установок .NET. При таких обстоятельствах выполняемая копия caspol.exe будет изменять политику безопасности только для связанной с ней установкой .NET. Чтобы сохранить простым управление политикой безопасности, вполне можно удалить предыдущие копии .NET при установке последующих версий.

Просмотр групп кода сборки

Сборки соответствуют группам кода в зависимости от условий членства. Если вернуться к примеру групп кода и загрузить сборку с web-сайта https://intranet/, она будет соответствовать группам кода таким образом:

Сборка является членом корневой группы кода (All Code); так как она приходит из локальной сети, то она является также членом группы кода Intranet, но вследствие того, что она была загружена со специального сайта https://intranet, то ей также предоставляется FullTrust, что соответствует выполнению без ограничений.

Легко увидеть группы кода, членом которых является сборка, используя команду:

caspol.exe -resolvegroup assembly.dll

Выполнение этой команды для сборки на локальном диске создает вывод:

Microsoft (R) .NET Framework CasPol 1.0.2728.0

Copyright (с) Microsoft Corp 1999-2001. All rights reserved.

Level = Enterprise

Code Groups:

1. All code: FullTrust

Level = Machine

Code Groups:

1. All code: Nothing

1.1. Zone — MyComputer: FullTrust

Level = User

Code Groups:

1. All code: FullTrust

Success

Можно заметить, что группы кода перечислены на трех уровнях — Enterprise, Machine и User (Предприятие, Машина и Пользователь). В данный момент сосредоточимся только на уровне Machine, два других более подробно рассмотрим позже. Если вы желаете знать об отношениях между тремя уровнями, то эффективные полномочия, предоставляемые сборке, являются пересечением полномочий из трех уровней. Например, если удалить полномочие FullTrust из зоны Internet политики на уровне Enterprise, то все полномочия отменяются для кода из зоны Internet и настройки двух других уровней становятся неподходящими.

Теперь используем эту команду для той же сборки, но через HTTP на удаленном сервере. Мы увидим, что сборка является членом различных групп, которые имеют более ограничительные права:

caspol.exe -resolvegroup http://server/assemply.dll

Microsoft (R) .NET Framework CasPol 1.0.2728.0

Copyright (с) Microsoft Corp 1999-2001. All rights reserved.

Level = Enterprise

Code Groups:

1. All code: FullTrust

Level = Machine

Code Groups:

1. All code: Nothing

1.1. Zone — Internet: Internet

1.1.1. All code: Same site Socket and Web.

Level = User

Code Groups:

1. All code: FullTrust

Success

Для сборки в этот раз можно видеть, что пересечение полномочий оставляет полномочия Internet и Same site Socket.

Полномочия доступа к коду и множества полномочий

Представьте себя администратором политики безопасности в сети персональных машин большого предприятия. В такой среде весьма полезно для CLR собирать свидетельства о коде, прежде чем его выполнять, но в равной степени администратор должен иметь возможность строго контролировать, что разрешается делать коду на нескольких сотнях машин, которыми он управляет, если CLR знает, откуда он пришел. Именно здесь начинают играть свою роль полномочия.

После того как сборка сопоставлена с группами кода, CLR просматривает политику системы безопасности для определения предоставляемых сборке полномочий. Это на самом деле похоже на систему безопасности учетных записей пользователей в Windows 2000. Полномочия обычно применяются не к пользователям, а к группам. То же самое справедливо для сборок: полномочия применяются к группам кода, а не к отдельным сборкам, что делает управление политикой системы безопасности в .NET гораздо более простой задачей.

Политика безопасности определяет, какие действия разрешается выполнять сборками в группе кода. Давайте обсудим полномочия доступа к коду, предоставляемые CLR. Нижеследующий список показывает, какие возможности предоставляют полномочия доступа к коду:

□ DirectoryServicesPermission — получение доступа к активному каталогу (Active Directory) с помощью классов System.DirectoryServices.

□ DnsPermission — использование системы имен доменов TCP/IP (DNS).

□ EnvironmentPermission — чтение и запись переменных окружения.

□ EventLogPermission — чтение и запись в журнал событий.

□ FileDialogPermission — доступ к файлам, которые были выбраны пользователем в диалоговом окне Open.

□ FileIOPermission — работа с файлами (чтение, запись и добавление в файл, а также создание и изменение папок).

□ IsolatedStorageFilePermission — доступ к закрытым виртуальным файловым системам.

□ IsolatedStoragePermission — доступ к изолированной памяти; памяти, которая ассоциируется с отдельным пользователем и с некоторыми аспектами идентичности кода, такими как его web-сайт, сигнатура или издатель.

□ MessageQueuePermission — использование очереди сообщений с помощью Microsoft Message Queue.

□ OleDbPermission — доступ к базам данных с помощью OLE DB.

□ PerformanceCounterPermission — использование показателей производительности.

□ PrintingPermission — доступ к печати.

□ ReflectionPermission — доступ к информации о типе с помощью System.Reflection.

□ RegistryPermission — чтение, запись, создание или удаление ключей и значений в реестре.

□ SecurityPermission — выполнение, объявление полномочий, обращение к неуправляемому коду, пропуск проверки, и другие полномочия.

□ ServiceControllerPermission — получение доступа (для выполнения или остановки) к службам Windows.

□ SocketPermission — создание или принятие соединения TCP/IP на транспортном адресе.

□ SQLClientPermission — доступ к базам данных SQL.

□ UIPermission — доступ к интерфейсу пользователя.

□ WebPermission — осуществление или принятие соединения с/из Web.

С помощью любого из этих классов полномочий определяется еще более глубокий уровень детализации. Позже будет показан пример, запрашивающий не просто доступ к файлу, а определенный уровень такого доступа.

С практической точки зрения настоятельно рекомендуется все попытки использования ресурсов, связанных с полномочиями в этом списке, помещать внутри блоков обработки ошибок try-catch, чтобы приложение ухудшалось постепенно, если ему придется выполняться с ограниченными полномочиями. Конструкция приложения должна определять, как приложение будет действовать в такой ситуации, не стоит предполагать, что оно начнет выполняться с такой же политикой системы безопасности, с которой оно разрабатывалось. Например, если приложение не может обратиться к локальному диску, закончится ли оно или должно действовать другим способом?

Сборка связывается с несколькими группами кода, действующие полномочия сборки в рамках политики системы безопасности являются объединением всех полномочий, предоставленных всем группам кода, которым эта сборка принадлежит. В результате, каждая группа кода, которой соответствует сборка, будет расширять набор допустимых действий сборки. Отметим, что группы кода, расположенные ниже в дереве, часто присваивают более свободные полномочия, чем расположенные выше.

Существует другое множество полномочий, которыми располагает CLR на основе идентичности кода. Эти права не могут быть предоставлены явно, так как они связаны непосредственно со свидетельством, которое CLR сопоставляет со сборкой, и называются полномочиями идентичности (Identity Permisssions). Вот имена классов для полномочий идентичности:

□ PublisherIdentityPermission — цифровая подпись издателя программного обеспечения.

□ SiteIdentityPermission — расположение web-сайта, из которого получен код.

□ StrongNameIdentityPermission — устойчивое имя сборки.

□ URLIdentityPermission — URL, откуда получен код (включая протокол, например https://)

□ ZoneIdentityPermission — зона, являющаяся местом происхождения сборки.

Обычно полномочия применяются блоками, вот почему .NET предоставляет также множества полномочий (Permission Sets). Это списки прав доступа к коду, сгруппированные в именованном множестве. Вот готовые именованные множества полномочий, существующие в системе:

□ FullTrust — никаких ограничений на полномочия.

□ Execution — возможность выполнения, но без доступа к каким-либо защищенным ресурсам.

□ Nothing — никаких полномочий и невозможность выполнения.

□ LocalIntranet — политика по умолчанию для локальной интранет, подмножество полного множества полномочий. Можно изменять это множество полномочий.

□ Internet — политика по умолчанию для кода неизвестного происхождения. Администратор может управлять полномочиями в этом множестве полномочий.

□ Everything — все стандартные полномочия, за исключением полномочия пропускать проверку кода. Администратор может изменить чье-либо право в этом множестве полномочий. Это полезно там, где политика по умолчанию должна быть строже.

Отметим, что из этих именованных множеств можно изменять определения только трех последних, а первые три являются фиксированными и не могут изменяться.

Полномочия идентичности не могут включаться в множества полномочий, так как CLR является единственным уполномоченным, который может предоставить коду права идентичности. Например, если фрагмент кода принадлежит определенному издателю, то мало смысла со стороны администратора предоставлять ему полномочия идентичности, связанные с другим издателем. CLR дает права, если необходимо, а мы может затем при желании их использовать.

Просмотр полномочий сборки

Предположим, что используется приложение, написанное компанией Microsoft, и мы пытаемся использовать свойство, которое раньше не использовалось. Приложение не имеет копии кода, хранящейся локально, поэтому оно запрашивает ее, и код загружается в глобальный кэш сборок. При таком сценарии для кода из Интернета, опубликованного указанной организацией, подписавшей сборку с помощью сертификата, мы обнаружим, что членство группы кода сборки выглядит примерно так:

Согласно политике в этом примере, группы кода All Code и Internet предоставляют только ограниченные полномочия, а членство группы кода в правом нижнем углу предоставляет сборке полномочие FullTrust. Права всех групп кода объединяются в создании общего эффективного полномочия самого высокого уровня. Таким образом, каждая группа кода, к которой принадлежит сборка, вносит дополнительные полномочия.

Кроме групп кода, к которым принадлежит сборка, мы можем наблюдать полномочия, присвоенные группам кода, а также полномочия идентичности кода, которые дают нам доступ к свидетельству, представленному кодом во время выполнения. Чтобы увидеть полномочия для групп кода сборки, используется команда:

caspol.exe -resolveperm assembly.dll

Проверим это на сборке и визуально оценим предоставленные ей полномочия идентичности и доступа к коду, когда мы получаем к ней доступ через локальную интранет. Если мы введем следующую команду, то увидим полномочия доступа к коду и затем в конце три полномочия идентичности:

caspol.exe -resolveperm http://intranet/assembly.dll

Microsoft (R) .NET Framework CasPol 1.0.xxxx.x

Copyright (c) Microsoft Corp 1999-2001. All rights reserved.

Resolving permissions for level = Enterprise

Resolving permissions for level = Machine

Resolving permissions for level = User

Grant =

<PermissionSet class="System.Security.PermissionSet" version="1">

<IPermission class="System.Security.Permissions.EnvironmentPermission, mscorlib, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Read="USERNAME;TEMP;TMP" />

<IPermission class ="System.Security.Permissions.FileDialogPermission, mscorlib, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Unrestricted="true" />

<IPermission class="System.Security.Permissions.IsolatedStorageFilePermission, mscorlib Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Allowed="AssemblyIsolationByUser"

UserQuota="9223372036854775807"

Expiry = "9223372036854775807"

Permanent="True" />

<IPermission class="System.Security.Permissions.ReflectionPermission, mscorlib, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Flags="ReflectionEmit" />

<IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Flags="Assertion, Execution, RemotingConfiguration" />

<IPermission class="System.Security.Permissions.UIPermission, mscorlib, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Unrestricted="true" />

<IPermission class="System.Net.WebPermission, System, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1">

<ConnectAccess>

<URI uri="(https|http)://intranet/.*"/>

</ConnectAccess>

</IPermission>

<IPermission class="System.Net.DnsPermission, System, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Unrestricted="true"/>

<IPermission class="System.Drawing.Printing.PrintingPermission, System.Drawing, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

version="1"

Level="DefaultPrinting" />

<IPermission сlass="System.Diagnostics.EventLogPermission, System, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1">

<Machine name="." access="Instrument" />

</IPermission>

<IPermission class="System.Security.Permissions.SiteIdentityPermission, mscorlib, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Site="intranet" />

<IPermission class="System.Security.Permissions.UrlIdentityPermission, mscorlib, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Url="http://intranet/assembly.dll" />

<IPermission сlass="System.Security.Permissions.ZoneIdentityPermission, mscorlib, Version=1.0.xxxx.x, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Zone="Intranet" />

</PermissionSet>

Success

Этот вывод показывает каждое из полномочий в виде кода XML, включая определяющий полномочие класс, содержащую класс сборку, версию полномочия и признак шифрования. Вывод предполагает, что можно создать свои собственные полномочия, больше об этом будет сказано позже. Можно также видеть, что каждое из полномочий идентичности содержит более подробную информацию, скажем, о классе UrlIdentityPermission, предоставляющем доступ к URL кода.

Отметим, что в начале вывода caspol.exe разрешает полномочия на уровнях Enterprise, Machine и User и затем перечисляет действующие представленные права. Теперь перейдем к этому вопросу.

Уровни политики: машина, пользователь и предприятие

До сих пор безопасность рассматривалась в контексте одной машины. Часто необходимо определить политику системы безопасности для определенных пользователей или для всей организации, и именно поэтому .NET предоставляет не один, а три уровня групп кода:

□ Machine (Машина)

□ Enterprise (Предприятие)

□ User (Пользователь)

Уровни групп кода управляются независимо и существуют параллельно:

Если существует три вида политики безопасности, как узнать, какая применяется? Действующим полномочием является пересечение полномочий трех уровней. Каждый из трех уровней имеет возможность запретить права, предоставленные другими,— это хорошая весть для администраторов, так как их настройки будут переопределять настройки пользователя.

Для работы с группами кода и полномочиями на уровнях пользователя или предприятия с помощью caspol.exe добавьте либо аргумент enterprise, либо аргумент user, чтобы изменить режим команды, caspol.exe работает по умолчанию на уровне Machine; таким образом эта утилита до сих пор и использовалась. Просмотрим группы кода, перечисленные на уровне User:

caspol.exe -user -listgroups

Так выглядит вывод команды при установке по умолчанию:

Security is ON

Execution checking is ON

Policy change prompt is ON

Level = User

Code Groups:

1. All code: FullTrust

Success

Теперь выполним ту же самую команду, но в этот раз, чтобы увидеть группы кода на уровне Enterprise:

caspol.exe -enterprise -listgroups

Вывод команды выглядит так:

Security is ON

Execution checking is ON

Policy change prompt is ON

Level = Enterprise

Code Groups:

1. All code: FullTrust

Success

Как можно видеть, по умолчанию оба уровня, User и Enterprise, конфигурируются с предоставлением FullTrust для единственной группы кода All Code. В результате этого настройка по умолчанию для системы безопасности .NET не налагает никаких ограничений на уровне пользователя или предприятия, и реализованная политика диктуется исключительно политикой уровня машины. Например, если требуется применить более строгое полномочие или множество полномочий к уровню пользователя или предприятия, эти ограничения будут налагаться на все полномочия и, возможно, переопределять права на уровне машины. Действующие полномочия являются пересечением, поэтому, например, если необходимо применить FullTrust к группе кода, это полномочие должно быть присвоено группе кода на каждом из трех уровней политики.

При выполнении caspol.exe администратором по умолчанию используется уровень машины, но если выйти из системы и зарегистрироваться как пользователь, который не принадлежит группе пользователей Administrator, caspol.exe выберет по умолчанию уровень пользователя. Кроме того, caspol.exe не позволит изменить политику безопасности таким образом, чтобы сделать саму утилиту caspol.exe неработоспособной.

Теперь мы получили общее представление об архитектуре безопасности в .NET и можем перейти к обсуждению, как получить доступ к ее свойствам программным путем.

Поддержка безопасности в .NET

Чтобы система безопасности .NET работала, программистам необходимо доверить CLR обеспечение политики безопасности. Как это сделать? Когда вызывается метод, требующий специальных полномочий (например, доступ к файлу на локальном диске), CLR будет просматривать стек с целью гарантии того, что участник цепочки вызовов имеет требуемые права.

Слово 'производительность' уже, вероятно, обеспокоило вас в данный момент, и понятно, что ее повышение — это проблема, но чтобы получить преимущества управляемой среды .NET, приходится платить за безопасность. Иначе сборки, не являющиеся полностью надежны ни, могли бы делать обращения к надежным сборкам и система была бы открыта для атаки.

Для справки, наиболее применимыми в этой главе частями библиотеки .NET Framework являются:

□ System.Security.Permissions

□ System.Security.Policy

□ System.Security.Principal

Отметим, что система безопасности доступа к коду на основе свидетельства работает в паре с системой безопасности регистрации в Windows. Если вы захотите выполнить приложение для настольного компьютера .NET, то ему должны быть предоставлены соответствующие полномочия системы безопасности доступа к коду .NET, но регистрирующийся пользователь должен также работать под учетной записью Windows, которая имеет соответствующие права для выполнения кода. В применении к настольным приложениям это означает, что текущему пользователю должны быть предоставлены соответствующие права для доступа к соответствующим файлам сборки на диске. Для приложений Интернета учетная запись, с которой работает Информационный сервер Интернет, должна иметь доступ к файлам сборки.

Требуемые полномочия

Создадим приложение Windows Forms с кнопкой, которая при нажатии будет выполнять действие, обращающееся к диску. Предположим, что если приложение не имеет подходящего полномочия для доступа к локальному диску (FileIOPermission), кнопка будет помечаться как недоступная (серым цветом).

В следующем коде представлен конструктор формы, который создает объект FileIOPermission, вызывает его метод Demand() и затем обрабатывают результат:

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

using System.Security;

using System.Security.Permissions;

namespace SecurityApp4 {

public class Form1 : System.Windows.Forms.Form {

private System.Windows.Forms.Button button1;

private System.ComponentModel.Container components;

public Form1() {

InitializeComponent();

try {

FileIOPermission fileioperm = new

FileIOPermission(FileIOPermissionAccess.AllAccess, @"C:\");

fileioperm.Demand();

} catch {

button1.Enabled = false;

}

}

public override void Dispose() {

base.Dispose();

if(component != null) components.Dispose();

}

#region Windows Form Designer generated code

/// <summary>

/// Требуемый метод для поддержки конструктора — не изменяйте

/// содержимое этого метода с помощью редактора кода.

/// </summary >

private void InitializeComponent() {

this.button1 = new System.Windows.Forms.Button();

this.SuspendLayout();

//

// button1

//

this.button1.Location = new System.Drawing.Point(48, 104);

this.button1.Name = "button1";

this.button1.Size = new System.Drawing.Size(192, 23);

this.button1.TabIndex = 0;

this.button1.Text = "Button Requires FileIOPermission";

//

// Form1

//

this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);

this.ClientSize = new System.Drawing.Size(292, 273);

this.Controls.AddRange(new System.Windows.Forms.Control[] {this.button1});

this.Name = "Form1";

this.Text = "Form1";

this.ResumeLayout(false);

}

#endregion

/// <summary>

/// Основная точка входа для приложения.

/// </summary >

[STAThread]

static void Main() {

Application.Run(new Form1());

}

}

}

Можно заметить, что FileIOPermission содержится в пространстве имен System.Security.Permissions, которое имеет все множество полномочий, а также предоставляет классы для декларативных атрибутов полномочий и перечисления параметров, используемых при создании объектов прав (например, при создании FileIOPermission, определяющего, нужен нам полный доступ или только для чтения).

При выполнении приложения с локального диска, где используемая по умолчанию политика безопасности разрешает доступ к локальной памяти, приложение будет выглядеть следующим образом:

Но если скопировать исполняемый файл на сетевой диск общего доступа и снова его выполнить, то он будет действовать внутри множества полномочий LocalIntranet, которое блокирует доступ к локальной памяти, и кнопка будет неработоспособной (серой):

Если при нажатии на кнопку была реализована функциональность для доступа к диску, не требуется писать никакой код для системы безопасности, так как соответствующий класс в .NET Framework будет запрашивать полномочия файла и CLR гарантирует, что каждый вызывающий в стеке будет иметь эти полномочия, прежде чем продолжить. При выполнении приложения из интранет, которое пытается открыть файл на локальном диске, будет порождаться исключение при условии, что политика безопасности не была изменена.

При желании перехватывать исключения, порождаемые CLR, когда код пытается действовать вопреки предоставленным ему правам, можно перехватывать исключение типа SecurityException, которое предоставляет доступ к ряду полезных элементов информации, включая читаемую человеком трассировку стека (SecurityException.StackTrace) и ссылку на метод, порождающий исключение (SecurityException.TargetSite). SecurityException предоставляет также свойство SecurityException.PermissionType, возвращающее тип объекта Permission, который порождает исключения безопасности.

Запрашиваемые полномочия

Как мы видели выше, требуемые полномочия вполне четко определяют, что необходимо во время выполнения, однако можно сконфигурировать сборку так, чтобы она делала более мягкий запрос прав прямо в начале выполнения, где она объявляет, что ей нужно: Можно запросить полномочия тремя способами:

□ Минимальные полномочия (Mimimum) — полномочия, которые требуются коду для выполнения.

□ Необязательные полномочия (Optional) — полномочия, которые код может использовать, но способен эффективно выполняться и без них.

□ Непригодные полномочия (Refused) — полномочия, которые не должны предоставляться коду.

Почему необходимо запрашивать полномочия при запуске сборки? Существует несколько причин:

□ Если сборке требуются некоторые полномочия для выполнения, то имеет смысл сообщить об этом в начале, а не во время выполнения.

□ Будут предоставлены только запрашиваемые полномочия и никакие другие. Это сокращает риск воздействия сборки на области, для которых у нее нет прав.

□ Если запрашивается минимальный набор полномочий, увеличивается вероятность того, что сборка будет выполнена.

Запрос полномочий скорее всего будет тем нужнее, чем более сложная разработка реализуется, но существует высокий риск, что приложение будет установлено на машине, которая не предоставляет необходимых прав. Обычно для приложения более предпочтительно знать в самом начале выполнения, где ему не предоставлены полномочия, а не в середине процесса выполнения.

Чтобы успешно запрашивать полномочия для сборки, нужно точно отслеживать, какие права использует сборка. В частности, необходимо знать о требованиях полномочий вызовов, которые делает сборка в других библиотеках классов, включая .NET Framework.

Три примера из файла AssemblyInfo.cs (см. ниже) демонстрируют использование атрибутов для запроса полномочий. Эти примеры можно найти в проекте SecurityАрр9 среди загружаемых с сайта издательства Wrox файлов. Первый атрибут выдвигает требование, чтобы сборка имела UIPermission, что даст приложению доступ к интерфейсу пользователя. Запрос делается для минимальных полномочий, а если это право не предоставляется, то сборка не сможет запуститься.

Using System.Security.Permissions;

[assembly:UIPermissionAttribute(SecurityAction.RequestMimimum)]

Затем запрашивается, отказывается ли сборка от доступа к диску C:\ . Настройка атрибута означает, что для всей сборки будет заблокирован доступ к этому диску:

[assembly:FileIOPermissionAttribute(SecurityAction.RequestRefuse, Read="C:\\")]

Ниже дан атрибут, который запрашивает для сборки необязательные полномочия, чтобы обеспечить доступ к неуправляемому коду:

[assembly:SecurityPermissionAttribute(SecurityAction.RequestOptional,

Flags = SecurityPermissionFlag.UnmanagedCode)]

В данном сценарии можно было бы добавить этот атрибут в приложение, которое обращается к неуправляемому коду по крайней мере в одном месте. В таком случае мы определяем, что это полномочие является необязательным, предполагая, что приложение может выполняться без полномочия доступа к неуправляемому коду. Если сборке не предоставлены права для доступа к неуправляемому коду и она попытается это сделать, то будет порождаться исключение SecurityException, которое должно ожидаться приложением и соответственно обрабатываться.

При рассмотрении требований полномочий для приложения обычно необходимо выбрать одну из следующих возможностей:

□ Запрос всех необходимых полномочий в начале выполнения и постепенное снижение требований или выход, если эти полномочия не предоставлены.

□ Отказ от запроса полномочий в начале выполнения, но готовность обрабатывать исключения безопасности внутри приложения.

Если сборка была сконфигурирована для использования атрибутов полномочий таким образом, мы сможем использовать утилиту permview.exe для просмотра полномочий, нацеливая ее на файл сборки, содержащий манифест сборки:

permview.exe assembly.dll

Вывод для приложения, использующего три показанных атрибута, будет выглядеть так.

minimal permission set:

<PermissionSet class="System.Security.PermissionSet" version="1">

<IPermission class="System.Security.Permissions.UIPermission, mscorlib, Version=1 .0.2411.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1" Unrestricted="true" />

</PermissionSet>

optional permission set:

<PermissionSet class="System.Security.Permission.Set" version="1">

<IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=1.0.2411.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Flags="UnmanegedCode" />

</PermissionSet>

refused permission set:

<PermissionSet class="System.Security.PermissionSet" version="1" >

<IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=1.0.2411.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"

version="1"

Read="C:\" />

</PermissionSet>

В дополнение к запрашиваемым полномочиям можно также запросить целое множество прав сразу. Так как некоторые множества полномочий (Internet, LocalIntranet и Everything) изменяются с помощью политики безопасности при выполнении сборки, то они запрашиваться не могут. Например, если сборка сообщила, что ей необходимо предоставить для выполнения все полномочия в множестве полномочий LocalIntranet, а администратор затем сузил множество прав LocalIntranet во время выполнения приложения, то может быть неизвестно, в каком множестве прав происходит работа.

Существует три множества полномочий, которые нельзя изменить во время выполнения приложения, эти множества могут запрашиваться с помощью атрибутов:

□ Nothing

□ Execution

□ FullTrust

Вот пример того, как запрашивается встроенное множество полномочий:

[assembly:PermissionSetAttribute(SecurityAction.RequestMinimum,

Name = "FullTrust")]

В этом примере сборка запрашивает, как минимум, встроенное множество полномочий FullTrust. Если это множество не будет предоставлено, то сборка породит во время выполнения исключение безопасности.

Неявное полномочие

Часто, когда предоставлена некоторые полномочия, возникает неявное утверждение, что также даны и другие полномочия. Например, если присвоено полномочие FileIOPermission для C:\, то неявно предполагается, что также имеется доступ к его подкаталогам (допущение системы безопасности учетных записей Windows).

Если необходимо проверить, что данное полномочие неявно вносит другое полномочие в качестве подмножества, то можно сделать следующее.

// Пример из SecurityApp5

class Class1 {

static void Main(string[ ] args) {

CodeAccessPermission permissionA =

new FileIOPermission(FileIOPermissionAccess.AllAccess, @"C:\");

CodeAccessPermission permissionB =

new FileIOPermission(FileIOPermissionAccess.Read, @"C:\temp");

if (permissions.IsSubsetOf(permissionA) {

Console.WriteLine("PermissionB is a subset of PermissionA");

} else {

Console.WriteLine("PermissionB is NOT a subset of PermissionA");

}

}

}

Вывод будет выглядеть следующим образом:

PermissionB is a subset of PermissionA

Отказ от полномочий

Существуют ситуации, когда необходимо быть абсолютно уверенным, что вызываемый метод действует в защищенном окружении, где он не может сделать ничего неблагоприятного. Предположим, нужно сделать вызов класса независимого поставщика и при этом точно знать, что он не получит доступ к локальному диску.

Чтобы достичь этого, создается экземпляр полномочия, который не должен получить метод а затем перед вызовом класса вызывается метод Deny():

using System;

using System.IO;

using System.Security;

using System.Security.Permissions;

namespace SecurityApp6 {

class Class1 {

static void Main(string[] args) {

CodeAccessPermission permission =

new FileIOPermission(FileIOPermissionAccess.AllAccess, @"C:\");

permission.Deny();

UntruscworthyClass.Method();

CodeAccessPermission.RevertDeny();

}

}

class UntrustworthyClass {

public static void Method() {

try {

StreamReader din = File.OpenText(@"C:\textfile.txt");

}

catch {

console.WriteLine("Failed to open file");

}

}

}

}

Если выполнить этот код, то будет выведено сообщение Failed to open file, так как ненадежный класс не имеет доступа к локальному диску.

Отметим, что вызов Deny() делается на экземпляре класса полномочия, в то время как вызов RevertDeny() выполняется статически. Причина этого заключается в том, что вызов RevertDeny() возвращает в прежнее состояние все запросы Deny() в рамках текущего стека. Таким образом, если было сделано несколько вызовов Deny(), то необходимо сделать только один последующий вызов RevertDeny().

Заявляемые полномочия

Пусть имеется сборка, которая была установлена в системе пользователя с полномочиями Full Trust. В этой сборке существует метод, который сохраняет контрольную информацию в текстовом файле на локальном диске. Если позже будет установлено приложение, для которого следует воспользоваться свойством контроля, то для приложения необходимо будет иметь соответствующие полномочия FileIOPermission для сохранения данных на диске.

Это кажется, однако, избыточным, так как в действительности необходимо выполнить крайне ограниченное действие на локальном диске. В такой ситуации будет полезно, если сборки с ограниченными полномочиями обратятся к более надежным сборкам, которые могут временно увеличить область действия полномочий на стек и выполнить операции от имени вызывающей стороны, которая не имеет на это прав.

В результате сборки с достаточно высоким уровнем надежности могут заявить требуемые ими полномочия. Если сборка имеет полномочия, которые нужны ей для заявки дополнительных прав, это снимает необходимость для вызывающих в стеке методов иметь такие широкие полномочия.

Приведенный ниже код содержит класс AuditClass, реализующий метод Save(), который получает строку и сохраняет контрольные данные в C:\audit.txt. Метод AuditClass заявляет полномочия, которые ему нужны для добавления контрольных строк в файл. Чтобы протестировать это, метод приложения Main() явно отвергает полномочие файла, которое требуется методу Audit:

using System;

using System.IO;

using System.Security;

using System.Security.Permissions;

namespace SecurityApp7 {

class Class1 {

static void Main(string[] args) {

CodeAccessPermission permission =

new FileIOPermission(FileIOPermissionAccess.Append, @"C:\audit.txt");

permission.Deny();

AuditClass.Save("some data to audit");

CodeAccessPermission.RevertDeny();

}

}

class AuditClass {

public static void Save (string value) {

try {

FileIOPermission permission =

new FileIOPermission(FileIOPermissionAccess.Append, @"C:\audit.txt");

permission.Assert();

FileStream stream new FileStream(@"C:\audit.txt", FileMode.Append, FileAccess.Write);

// код для записи файла контроля здесь

CodeAccessPermission.RevertAssert();

Console.WriteLine("Data written to audit file");

} catch {

Console.WriteLine("Failed to write data to audit file");

}

}

}

}

При выполнении этого кода оказывается, что вызов метода AuditClass не порождает исключения безопасности, несмотря на то, что при вызове он не имел требуемых полномочий для доступа к диску.

Как и в случае RevertDeny(), RevertAssert(), являясь статическим методом, отменяет все заявления в текущей среде выполнения. Важно быть очень осторожным при использовании заявлений. Мы явно присваиваем полномочия методу, вызываемому кодом, который вполне может не иметь этих полномочий. Например, в коде с контролем, даже если политика безопасности диктует, что установленные приложения не могут делать записей на локальный диск, данное приложение выполнит это, если функции контроля были установлены с полномочиями Full Trust.

Создание полномочий доступа к коду

.NET Framework реализует полномочия безопасности доступа к коду, которые предоставляют защиту для ресурсов. Однако в ситуациях, когда необходимо создать свои собственные полномочия, это можно сделать с помощью подклассов CodeAccessPermission. Вывод из этого класса предоставляет возможности системы безопасности доступа к коду из .NET, включая просмотр стека и управление политикой.

Вот два случая, когда может понадобиться создание своих собственных полномочий доступа к коду:

□ Защита ресурса, еще не защищенного с помощью Framework. Например, разработано приложение .NET для автоматизации, которое реализуется с помощью встроенного аппаратного устройства. Создавая свой собственный код полномочий доступа, можно получить достаточно развитый уровень управления доступом для данного устройства автоматизации.

□ Дополнительное ограничение существующих полномочий. Например, хотя .NET Framework дает права, допускающие точный контроль за доступом к локальной файловой системе, можно иметь приложение, где желательно контролировать доступ к определенному файлу или папке более жестко. При таком сценарии полезно создать полномочие доступа к коду, которое связано специально с этим файлом или папкой, и без этого полномочия никакой управляемый код не может получить доступ к этой области на диске.

Декларативная безопасность

Можно отказаться, запросить или заявить полномочия, вызывая классы в .NET Framework, но можно также использовать атрибуты и определять требования полномочий декларативно.

Основное достоинство использования декларативной безопасности состоит в том, что настройки доступны с помощью отражения. Возможность получить доступ к информации посредством отражения дает огромный выигрыш для системных администраторов, которые часто хотят видеть требования безопасности приложений.

Допустим, мы объявляем, что метод для выполнения должен иметь полномочие на чтение с C:\.

using System;

using System.Security.Permissions;

namespace SecurityApp8 {

class Class1 {

static void Main(string[] args) {

MyClass.Method();

}

}

[FileIOPermission(SecurityAction.Assert, Read="C:\\")]

class MyClass {

public static void Method() {

// реализация находится здесь

}

}

}

Помните, что если атрибуты применяются для заявления или запроса полномочий, невозможно перехватить исключения, возникающие в случае отказа действия, так как не существует обязательного кода, который можно поместить в предложение try-catch-finally.

Чтобы узнать обо всех доступных атрибутах, можно ознакомиться с перечислением System.Security.Permissions.SecurityAction.

Система безопасности на основе ролей

Как мы видели, система безопасности доступа к коду дает CLR возможность неявно решить, должен ли код выполняться и с какими полномочиями на основе свидетельства о коде. В дополнение к этому .NET предоставляет систему безопасности на базе ролей, которая определяет, может ли код выполнить действия на основе свидетельства о пользователе и его роли.

Система безопасности на основе ролей особенно полезна в ситуациях, где доступ к ресурсам является основным вопросом. Хорошим примером может служить отрасль финансов, где роли сотрудников определяют, к какой информации они имеют доступ и какие действия могут выполнить.

Система безопасности на основе ролей также является идеальной для использования в соединении с учетными записями Windows 2000, Microsoft Passport или специальным каталогом пользователя для управления доступом к ресурсам на основе Web. Например, web-сайт может ограничить доступ к своему содержимому, пока пользователь не зарегистрирует свои данные на этом сайте, и затем дополнительно предоставит доступ к специальному содержимому, только если пользователь является оплаченным подписчиком. Во многих отношениях ASP.NET делает систему безопасности на основе ролей проще, так как большая часть кода находится на сервере.

Например, если желательно реализовать службу Web, требующей аутентификации, то можно использовать подсистему учетных записей Windows 2000 и написать метод для Web таким образом, чтобы он проверял, является ли пользователь членом определенной группы пользователей Windows 2000, прежде чем предоставлять доступ к функциональности метода.

Принципал

.NET предоставляет текущему потоку выполнения простой доступ к пользователю приложения, который обозначается как Principal. Принципал является ядром системы безопасности, предоставленной .NET, на основе ролей, и через него можно получить доступ к объекту Identity пользователя, который отображается в учетной записи пользователя одного из приведенных ниже типов:

□ Учетная запись Windows

□ Учетная запись Passport

□ Пользователь, аутентифицированный с помощью cookie из ASP.NET

В качестве дополнительного бонуса система безопасности на основе ролей в .NET может создавать свои собственные принципалы, реализуя интерфейс IPrincipal. Если вы не полагаетесь на аутентификацию Windows, Passport или простую аутентификацию с помощью cookie, необходимо рассмотреть вопрос о создании своей собственной аутентификации с помощью специального класса principal.

С помощью доступа к принципалу можно делать выводы о безопасности на основе идентичности и ролей принципала. Роль является совокупностью пользователей, которые имеют одинаковые полномочия безопасности, и является единицей администрации пользователей. Например, при использовании аутентификации Windows будет применяться тип WindowsIdentity в качестве варианта Identity. Можно выбрать этот тип для выяснения, является ли пользователь членом определенной группы учетных записей пользователей Windows, а затем использовать эту информацию для решения, предоставить или отменить доступ к коду и ресурсам.

Обычно значительно легче управлять безопасностью, если доступ к ресурсам и функциональности разрешается на основе полномочий, а не ролей. Представьте сценарий, где имеется три метода, каждый из них предоставляет доступ к свойству, для которого требуется строгий контроль, чтобы гарантировать, что только авторизованному персоналу доступ к нему открыт. Если приложение имеет, скажем, четырех пользователей, то достаточно легко определить в каждом методе, какие пользователи могут и какие не могут получить доступ к методу. Однако представим ситуацию, где число свойств возрастает до девяти. Чтобы разрешить доступ для дополнительного пользователя, потенциально потребуется изменить каждый из девяти методов, хотя это является административной задачей. Даже хуже, так как пользователи меняются ролями в компании и понадобится изменять код каждый раз, когда это происходит. Если вместо этого реализовать систему, использующую роли, то можно просто добавлять и удалять пользователей из ролей, а не добавлять и удалять отдельных пользователей из приложения. Таким образом, выполнение приложения облегчается, и для каждого метода мы только ставим условие, чтобы пользователь был членом определенной группы. Ото также упрощает управление ролями, так как эту работу может делать администратор, а не разработчик приложения. Говоря проще, разработчик сосредоточивается на том, что, например, Managers, но не Secretaries, могут получить доступ к методу, а не на том, каковы возможности Julie и Bob, но не Conrad.

Система безопасности на основе ролей в .NET строится на системе безопасности из MTS и COM+ 1.0 и предоставляет гибкую среду, создающую ограждения вокруг разделов приложения, которые должны быть защищены. Если система COM+ 1.0 установлена на машине, ее безопасность на основе ролей будет интерпретироваться в NET, однако COM не требуется .NET на основе ролей для функционирования системы безопасности.

Принципал Windows

Создадим консольное приложение, предоставляющее доступ к принципалу в приложении. в котором мы хотим пользоваться описанной ниже учетной записью Windows. Нам необходимы пространства имен System.Security.Principal и System.Threading. Прежде всего нужно задать, что мы хотим, чтобы .NET автоматически соединял принципал с описанной ниже учетной записью Windows, так как в .NET это не происходит автоматически по соображениям безопасности. Наше задание:

using System;

using System.Security.Principal;

using System.Security.Permissions;

using System.Threading;

namespace SecurityApplication2 {

class Class1 {

static void Main(string[] args) {

AppDomain.CurrentDomain.SetPrincipalPolicy(

PrincipalPolicy.WindowsPrincipal);

Можно использовать метод WindowsIdentity.GetCurrent() для доступа к данным учетной записи Windows, однако такой способ подходит, когда необходимо взглянуть на принципал один раз. Если необходимо обратиться к принципалу несколько раз, то более эффективно задать политику так, чтобы текущий поток выполнения предоставлял доступ к принципалу. При использовании метода SetPrincipalPolicy определяется, что принципал в текущем потоке выполнения должен поддерживать объект WindowsIdentity. Добавим код для доступа к свойствам принципала из объекта Thread:

WindowsPrincipal principal =

(WindowsPrincipal)Thread.CurrentPrincipal;

WindowsIdentity identity = (WindowsIdentity)principal.Identity;

Console.WriteLine("IdentityTyрe:" + identity.ToString());

Console.WriteLine("Name:" + identity.Name);

Console.WriteLine(

"Users'?:" + principal.IsInRole("BUILTIN\\Users"));

Console.WriteLine(

"Administrators' ?: " +

principal.IsInRole(WindowsBuiltInRole.Administrator));

Console.WriteLine("Authenticated:" + identity.IsAuthenticated);

Console.WriteLine("AuthType:" + identity.AuthenticationType);

Console.WriteLine("Anonymous?:" + identity.IsAnonymous);

Console.WriteLine("Token:" + identity.Token);

}

}

}

Вывод из этого консольного приложения будет выглядеть похожим на следующий, в зависимости от конфигурации машины и ролей, ассоциированных с учетной записью, под которой вы зарегистрировались в системе:

IdentityType:System.Security.Principal.WindowsIdentity

Name:MACHINE\alaric

'Users'?:True

'Administrators'?:True

Authenticated:True

AuthType:NTLM

Anonymous?:False

Token:256

Очевидно, что крайне полезно иметь возможность получить так легко доступ к данным о текущем пользователе и его ролях, чтобы с помощью этой информации решать, какие действия выполнить и какие отменить. Роли и группы пользователей Windows предоставляют администраторам дополнительную возможность использовать стандартные утилиты администрирования пользователей, и обычно можно избежать изменения кода, когда изменяются роли пользователя. Рассмотрим роли более подробно.

Роли

Представим сценарий, где имеется приложение интранет, использующее учетные записи Windows. Система имеет группу Manager и группу Assistant, пользователи относятся к этим группам в зависимости от их роли в организации. Пусть приложение содержит свойство, выводящее информацию о сотрудниках, и желательно, чтобы к нему имела доступ только группа Manager. Можно легко применить код, который проверяет, является ли текущий пользователь членом группы Manager, и разрешать или запретить ему доступ на основе этого.

Однако, если в дальнейшем будет решено реорганизовать группы учетных записей и ввести группу Personnel, которая также имеет доступ к данным о сотрудниках, то возникнет проблема. Придется просмотреть весь код и обновить его, чтобы включить правила для этой новой группы.

Лучшим решением было бы создание полномочия, называемого, предположим, ReadEmployeeDetails, и присваивание его группам, где необходимо. Если код проверяет полномочие ReadEmployeeDetails, то обновление приложения с целью разрешить группе Personnel доступ к данным о сотрудниках, является просто вопросом создания группы, внесения в нее пользователей и присваивание группе полномочия ReadEmployeeDetails.

Система безопасности на основе декларативной роли

Так же, как в случае с системой безопасности доступа к коду, можно реализовать запросы безопасности на основе ролей ("пользователь должен быть в группе Administrators"), используя обязательные запросы (см. предыдущий раздел), или использовать атрибуты. Возможно декларативное определение требований к полномочию на уровне класса в таком виде:

using System;

using System.Security;

using System.Security.Principal;

using System.Security.Permissions;

namespace SecurityApp3 {

class Class1 {

static void Main(string[] args) {

AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

try {

ShowMessage();

} catch (SecurityException exception) {

Console.WriteLine(

"Security exception caught (" + exception.Message + ")");

Console.WriteLine(

"The current principal must be in the local"

+ "Users group");

}

}

(PrincipalPermissionAttribute(SecurityAction.Demaid, Role =

"BUILTIN\\Users"));

static void ShowMessage() {

Console.WriteLine("The current principal is longed in locally");

Console.WriteLine("they are a member of the local Users group)");

}

}

}

Метод ShowMessage() будет порождать исключение, если приложение выполняется не в контексте пользователя из группы локальных Users в Windows 2000. Что касается приложений Web, то учетная запись, под которой выполняется код ASP.NET, должна быть в группе, хотя в реальном мире определенно будут избегать добавления этой учетной записи в группу администраторов.

Если выполнить приведенный выше код с помощью учетной записи в локальной группе Users, то вывод будет таковым:

The current principal is logged in locally

(they are a member of the local Users group)

Дополнительную информацию о системе безопасности на основе ролей в .NET можно найти в документации MSDN для пространства имен System.Security.Principal.

Управление политикой системы безопасности

Аспекты безопасности .NET разработаны значительно полнее и шире, чем какие-либо другие вопросы в Windows, но все же есть и которые ограничения, о которых необходимо знать:

□ Политика системы безопасности .NET не обеспечивает безопасность на неуправляемом коде (хотя обеспечивает некоторую защиту от обращении к неуправляемому коду).

□ Если пользователь копирует сборку на свою локальную машину, сборка имеет полномочия FullTrust и, таким образом, политика безопасности не действует. Чтобы решить эту проблему, можно ограничить полномочия предоставляемые локальному коду.

□ Политик системы безопасности .NET предоставляет очень небольшую помощь при борьбе со злонамеренными файлами .ЕХЕ Win32 и вирусами на основе сценариев, с которыми Microsoft борется различными способами. Например недавние версии Outlook не позволяют обрабатывать исполнимые файлы из почтовых сообщений, пользователь предупреждается, что они могут содержать вирусы, и сохраняет их на диске, где имеются возможности для установки административных ограничений, включая блокирование доступа к локальному диску и работу антивирусного программного обеспечения.

Однако .NET существенно помогает операционной системе в ответах на вопросы, сколько полномочий предоставить коду, будет ли он приложением интранет, элементом управления на странице Web или приложением Windows Forms, загруженным от поставщика программного обеспечения в Интернете.

Конфигурационный файл системы безопасности

Как мы уже видели, общим звеном, соединяющим группы кода, полномочия и множества полномочий, являются три уровня политики системы безопасности (Enterprise, Machine и User). Информация о конфигурации системы безопасности в .NET хранится в конфигурационных файлах XML, которые защищены системой безопасности Windows. Например, политика безопасности уровня Machine распространяется только на пользователей групп Administrator, Power User и SYSTEM в Windows 2000.

В Windows 2000 файлы, которые хранят политику безопасности расположены в следующих местах:

Конфигурация политики предприятия

C:\WinNT\Microsoft.NET\Framework\v1.0.xxxx\Config\enterprise.config

Конфигурация политики машины

C:\WinNT\Microsoft.NET\Framework\v1.0.xxxx\Config\security.config

Конфигурация политики пользователя

%USERPROFILE%\application data\Microsoft\CLR security config\vxx.xx\security.config

Номер версии, отмеченный несколькими 'x', будет меняться в зависимости от версии .NET Framework, установленной на машине. При необходимости можно вручную редактировать эти конфигурационные файлы, например, если администратору нужно сконфигурировать политику для пользователя не регистрируясь в системе со своей учетной записью. Однако в общем рекомендуется использовать caspol.exe или подключаемый модуль ММС для управления политикой системы безопасности.

Простой пример

Создадим небольшое приложение, обращающееся к локальному диску, поведение которого требует тщательного управления. Приложение относится к C# Windows Forms с окном списка и кнопкой. Если нажать на кнопку, то окно списка заполняется из файла с именем animals.txt в корне диска C:\:

Приложение создается с помощью Visual Studio.NET, единственными изменениями в нем являются добавления к форме окна списка и кнопки Load Data, а также подключение к кнопке события, что выглядит следующим образом.

// Пример из SecurityAppl

private void button1_Click(object sender, System.EventArgs e) {

StreamReader stream = File.OpenText(@"C:\animals.txt");

String str;

while ((str = stream.ReadLine()) != null) {

listBox1.Items.Add(str);

}

}

Метод открывает простой текстовый файл из корня диска C:\, который содержит список животных на отдельных строках, и загружает каждую строку в переменную типа string, которая затем используется для создания каждого элемента в окне списка.

Если выполнить приложение на локальной машине и нажать кнопку, то мы увидим загруженные из корня диска C:\ данные, выведенные в окне списка, как и ожидалось. Неявно среда выполнения предоставила сборке полномочие, которое необходимо для выполнения, доступа к интерфейсу пользователя и чтения данных с локального диска:

Вспомним, что полномочия группы кода в зоне интранет являются более строгими, чем на локальной машине, в частности, они не разрешают доступ к локальному диску (за исключением папки, из которой выполняется приложение). Если снова выполнить приложение, но в этот раз с общего сетевого диска, оно будет выполняться так же, как и раньше, поскольку ему предоставлены полномочия для выполнения и доступа к интерфейсу пользователя. Однако, если теперь нажать кнопку Load Data на форме, будет порождаться исключение безопасности:

В тексте сообщения исключения упоминается объект System.Security.Permissions.FileIOPermission. Он является полномочием, не предоставленным приложению и запрошенным классом из Framework, который используется для загрузки данных из файла на локальном диске.

По умолчанию группе кода Intranet предоставляется множество полномочий LocalIntranet. Изменим с помощью следующей команды это множество полномочий на FullTrust, чтобы любой код из зоны интранет мог выполняться полностью без ограничений.

caspol.exe -chggroup 1.2 FullTrust

Если теперь снова выполнить приложение с общего сетевого диска и нажать на кнопку, мы увидим, что окно списка заполняется из файла в корне диска C:\ и исключения не возникают.

В подобных сценариях, где используются ресурсы, управляемые полномочиями, рекомендуется расширять код, чтобы исключения безопасности перехватывались и приложение могло постепенно уменьшать свою деятельность. Например, в данном приложении можно добавить блок try-catch вокруг кода доступа к файлу и, если порождается исключение SecurityException, то в окне списка выводится строка "Permission denied accessing file" ("Полномочия не разрешают доступ к файлу"):

// Код из SecurityАрр1

private void button1_Click(object sender, System.EventArgs e) {

try {

StreamReader din = File.OpenText(@"C:\animals.txt");

String str;

while((str = din.ReadLine() != null) {

listBox1.Items.Add(str);

}

} catch (SecurityException exception) {

listBox1.Items.Add("Permission denied accessing file");

}

}

В действительности, если необходимо выполнить определенное приложение с общего сетевого диска, то скорее всего будет выбрано решение, которое не открывает клиентскую машину всему коду в интранет. Вместо этого будут использоваться группы кода и условия членства для строгого контроля требования приложения с учетом его расположения в интранет, строгого имени или сертификата, подтверждающего идентичность издателя.

Управление группами кода и полномочиями

В управляемой безопасности .NET, если сборка отказывает с исключением безопасности, обычно имеются три пути продолжения работы:

□ Смягчить политику полномочий

□ Переместить сборку

□ Применить для сборки строгое имя

Принимая решения такого рода, нужно брать в расчет уровень надежности сборки.

Включение и выключение системы безопасности

По умолчанию система безопасности .NET обычно включена. Если по какой-то причине необходимо ее выключить, это делается таким образом:

caspol.exe -security off

Чтобы снова включить систему безопасности, используйте команду:

caspol.exe -security on

Риски безопасности, присущие открытию машины при отключении системы безопасности, для нас означают, что систему безопасности необходимо отключать только для тестирования и отладки. Необходимо знать, что приведенная выше команда не требует административных привилегий, т.е. любой пользователь (или вирус) может отключить систему безопасности .NET. Поэтому настоятельно рекомендуется включать систему безопасности Windows, чтобы защититься против злонамеренного или необдуманного использования.

Восстановление политики системы безопасности

Если необходимо восстановить конфигурацию системы безопасности в ее первоначальном состоянии, можно ввести команду:

caspol.exe -reset

Эта команда переводит политику системы безопасности в состояние установки по умолчанию.

Создание группы кода

Можно создавать свои собственные группы кода и затем применять к ним определенные полномочия. Например, мы определяем, что хотим доверять любому коду с web-сайта www.wrox.com и предоставляем ему полный доступ к системе (не доверяя коду с любого другого web-сайта). Прежде всего необходимо получить числовую метку группы кода, в которой будет находиться новая группа кода.

caspol.exe -listgroups

Эта команда выводит приблизительно:

Code Groups:

1. All code: Nothing

1.1. Zone — MyComputer: FullTrust

1.2. Zone — Intranet: LocalIntranet

1.2.1. All code: Same site Socket and Web.

1.2.2. All code: Same directory FileIO — Read, PathDiscovery

1.3. Zone - Internet: Internet

1.3.1. All code: Same site Socket and Web.

1.4. Zone - Untrusted: Nothing

1.5. Zone — Trusted: Internet

1.5.1. All code: Same site Socket and Web.

…

Используя тот факт, что Zone: Internet помечена как 1.3, вводим команду:

caspol.exe -addgroup 1.3 -site www.wrox.com FullTrust

Отметим, что эта команда будет спрашивать подтверждение при попытке явно изменить политику системы безопасности на машине. Если теперь снова выполнить команду caspol.exe -listgroups, можно увидеть, что была добавлена новая группа кода, которой присвоено FullTrust.

Code Groups:

1. All code: Nothing

1.1. Zone - MyComputer: FullTrust

1.2. Zone — Intranet: LocalIntranet

1.2.1. All code: Same site Socket and Web.

1.2.2. All code: Same directory FileIO - Read, PathDiscovery

1.3. Zone — Internet: Internet

1.3.1. All code: Same site Socket and Web.

1.3.2. Site - www.wrox.com: FullTrust

1.4. Zone — Untrusted: Nothing

1.5. Zone - Trusted: Internet

1.5.1. All code: Same site Socket and Web.

1.5.2.

…

В другом примере предположим, что мы хотим создать группу кода в группе кода Intranet (1.2), которая предоставляет FullTrust всем приложениям, выполняющимся с определенного общего сетевого диска:

caspol.exe -addgroup 1.2 -url file:///\\intranetserver/sharename/*FullTrust

Удаление группы кода

Чтобы удалить созданную группу кода, можно ввести команду:

caspol.exe -remgroup 1.3.2

Она будет запрашивать подтверждение того, что вы хотите изменить политику системы безопасности, и если ответить положительно, она сообщит, что группа удалена.

Необходимо знать, что нельзя удалить группу кода All Code, но можно удалить группы кода на уровень ниже, включая группы для Internet, MyComputer и LocalIntranet.

Изменение полномочий группы кода

Чтобы ослабить или ограничить полномочия, присвоенные группе кода, можно снова использовать caspol.exe. Предположим, что мы хотим применить FullTrust к зоне Intranet. В этом случае сначала мы должны получить метку, которая представляет группу кода Intranet:

caspol.exe -listgroups

Вывод показывает группу кода Intranet:

Code Groups:

1. All code: Nothing

1.1 Zone - MyComputer: FullTrust

1.2. Zone — Intranet: LocalIntranet

1.2.1. All code: Same site Socket and Web.

1.2.2. All code: Same directory FileIO - Read, PathDiscovery

1.3. Zone - Internet: Internet

1.3.1. All code: Same site Socket and Web.

1.4. Zone — Untrusted: Nothing

1.5. Zone — Trusted: Internet

1.5.1. All code: Same site Socket and Web.

…

После получения метки группы кода Intranet вводим вторую команду, чтобы изменить полномочия группы кода:

caspol.exe -chggroup 1.2 FullTrust

Команда будет запрашивать подтверждение изменения политики системы безопасности, и если теперь снова выполнить команду caspol.exe -listgroups, можно будет увидеть, что полномочие в конце строки Intranet изменилось на FullTrust:

Code Groups:

1. All code: Nothing

1.1. Zone - MyComputer: FullTrust

1.2. Zone - Intranet: FullTrust

1.2.1. All code: Same site Socket and Web.

1.2.2. All code: Same directory FileIO - Read, PathDiscovery

1.3. Zone - Internet: Internet

1.3.1. All code: Same site Socket and Web.

1.4. Zone — Untrusted: Nothing

1.5. Zone - Trusted: Internet

1.5.1. All code: Same site Socket and Web.

…

Создание и применение множеств полномочий

Можно создавать новые множества полномочий с помощью команды:

caspol.exe -addpset CustomPermissionSet permissionset.xml

Эта команда определяет, что создается новое множество полномочий с именем CustomPermissionSet на основе содержимого указанного файла XML. Файл XML должен содержать стандартный формат, определяющий PermissionSet. Для справки здесь представлен файл множества полномочий для множества полномочий Everything, который можно сократить до требуемых размеров:

<PermissionSet class="NamedPermissionSet" version="1" Name="Everything"

Description="Allows unrestricted access to all resources covered by built in permissions">

<IPermission class="EnvironmentРеrmission" version="1" Unrestricted="true" />

<IPermission class="FileDialogPermission" version="1" Unrestricted="true" />

<IPermission class="FileIOPermission" version="1" Unrestricted="true" />

<IPermission class="IsolatedStorageFilePermission" version="1" Unrestricted="true" />

<IPermission class="ReflectionPermission" version="1" Unrestricted="true" />

<IPermission class="RegistryPermission" version="1" Unrestricted="true" />

<IPermission class="SecurityPermission" version="1"

Flags="Assertion, UnmanagedCode, Execution, ControlThread, ControlEvidence, ControlPolicy, SerializationFormatter, ControlDomainPolicy, ControlPrincipal, ControlAppDomain, RemotingConfiguration, Infrastructure" />

<IPermission class="UIPermission" version="1" Unrestricted="true" />

<IPermission class= "DnsPermission" version="1" Unrestricted="true" />

<IPermission class="PrintingPermission" version="1" Unrestricted="true" />

<IPermission class="EventLogPermission" version="1" Unrestricted="true" />

<IPermission class="SocketPermission" version="1" Unrestricted="true" />

<IPermission class="WebPermission" version="1" Unrestricted="true" />

<IPermission class="PerformanceCounterPermission" version="1" "Unrestricted="true" />

<IPermission class="DirectoryServicesPermission" version="1" Unrestricted="true" />

<IPermission class="MessageQueuePermission" version="1" Unrestricted="true" />

<IPermission class="ServiceControllerPermission" version="1" Unrestricted="true" />

</PermissionSet>

Чтобы увидеть все множества полномочий в формате XML, можно использовать команду:

caspol.exe -listpset

При желании применить конфигурационный файл PermissionSet XML к существующему множеству полномочий можно использовать команду:

caspol.exe -chgpset permissionset.xml CustomPermissionSet

Распространение кода с помощью строгого имени

.NET предоставляет возможность сопоставить сборку с группой кода, когда идентичность и целостность сборки подтверждена с помощью строгого имени. Этот сценарий широко используется при распространении сборок через сети, например распространение программного обеспечения через Интернет.

Если компания, производящая программное обеспечение, желает предоставить код своим заказчикам через Интернет, то создается сборка и для нее задается устойчивое имя. Устойчивое имя гарантирует, что сборка может быть идентифицирована уникальным образом, и также предоставляет защиту против подделки. Заказчики могут встроить это устойчивое имя в свою политику системы безопасности доступа к коду, а сборке, которая соответствует этому уникальному устойчивому имени, затем явно присвоить полномочия. Как мы видели в главе о сборках, устойчивое имя включает контрольные суммы для хэш-значений всех файлов в сборке, поэтому имеется строгое подтверждение, что сборка не была изменена с момента создания издателем устойчивого имени.

Отметим, что, если приложение использует программу установки, то эта программа будет устанавливать сборки, для которых уже задано устойчивое имя. Устойчивое имя создается однажды для каждого дистрибутива перед отправкой заказчику, программа установки не выполняет такие команды. Причина заключается в том, что устойчивое имя предоставляет гарантию, что сборка не была модифицирована с момента отправки из компании. Обычный способ достичь этого состоит в предоставлении заказчику не только кода приложения, но также отдельно — копию устойчивого имени сборки. Может оказаться полезным передавать устойчивое имя заказчику с помощью защищенной формы (возможно, факс или зашифрованное сообщение e-mail), чтобы защитить сборку от подделки в пути.

Давайте рассмотрим пример, где мы хотим создать сборку с устойчивым именем, которую можно распространять таким образом, чтобы получатель сборки смог использовать устойчивое имя для предоставления полномочия FullTrust.

Прежде всего необходимо создать пару ключей, так как устойчивые имена используют криптографию с открытым ключом. Открытый и закрытый ключи хранятся в указанном файле и используются для подписи устойчивого имени. Чтобы создать два ключа, используется sn.exe (Strong Name Tool — утилита устойчивого имени), которая помимо помощи в создании ключей, может также использоваться для управления ключами и устойчивыми именами. Попробуем создать ключ, для чего введем команду:

sn.exe -k key.snk

Затем файл с ключами (key.snk в данном случае) поместим в ту же папку, где Visual Studio создает файл вывода (обычно папка Debug), а затем ключ добавим в код с помощью атрибута сборки. Когда этот атрибут добавлен в AssemblyInfo.cs, остается только заново скомплектовать сборку. Перекомпиляция обеспечивает, новое вычисление хэш-значения и сборка защищается против злонамеренных модификаций:

[assembly: AssemblyKeyFileAttribute("key.snk")]

Итак, наша сборка откомпилирована и подписана, она имеет уникальное идентифицирующее устойчивое имя. Теперь можно создать новую группу кода на машине, где мы хотим выполнить сборку. Эта группа имеет условие членства, которое требует соответствия с устойчивым именем сборки.

Следующая команда утверждает, что мы собираемся создать новую группу кода, используя устойчивое имя из указанного файла манифеста сборки, а также что нас не интересует используемая версия сборки и что группе кода присваиваются полномочия FullTrust:

caspol.exe -addgroup 1 -strong -file \bin\debug\SecurityApp10.exe -noname -noversion FullTrust

Приложение в этом примере будет теперь выполняться из любой зоны, даже зоны Internet, так как устойчивое имя предоставляет достаточное свидетельство того, что сборка является надежной. Просмотрев группы кода с помощью caspol.exe -listgroups, увидим новую группу кода (1.8) и ее ассоциированный открытый ключ (в шестнадцатеричном представлении):

1. All code: Nothing

1.1. Zone — MyComputer: FullTrust

1.2. Zone — Intranet: LocalIntranet

1.2.1. All code: Same site Socket and Web.

1.2.2. All code: Same directory FileIO — Read, PathDiscovery

1.3. Zone — Internet: Internet

1.3.1. All code: Same site Socket and Web.

1.4. Zone — Untrusted: Nothing

1.5. Zone — Trusted: Internet

1.5.1. All code: Same site Socket and Web.

1.6. StrongName —

02400000480000094000000060200000024000052534131000400000100010007D1FA57C4AED9F0A32

E84AA0FAEFD0DE9E8FD6AEC8F87FB03766C834C99921EB23BE79AD9D5DCC1DD9AD236132102900B723

CF980957FC4E177108FC607774F29E8320E92EA05ECE4E821C0A5EFE8F1645C4C0C93C1AB99285D622

CAA652C1DFAD63D745D6F2DE5F17E5EAF0FC4963D261C8A12436518206DC093344D5AD293:

FullTrust

1.7. StrongName - 00000000000000000400000000000000: FullTrust

1.8. StrongName -

00240000048000009400000006020000002400005253413100040000010001007508D0780C56AF85BA

1BAD6D88E2C653E0A836286682C18134CC989546C1143252795A791F042238040F5627CCC1590ECEA3

0A9CD4780F5F0B29B55C375D916A33FD46B14582836E346A316BA27CD555B8F715377422EF589770E5

A5346A00BAABB70EF36774DFBCB17A30B67C913384E62A1C762CF40AFE6F1F605CCF406ECF:

FullTrust

Success

Для доступа к строгому имени в сборке можно применить утилиту secutil.exe с файлом манифеста сборки. Воспользуемся secutil.exe для просмотра данных устойчивого имени нашей сборки. Мы добавим параметр -hex, чтобы открытый ключ был показан в шестнадцатеричном представлении (как в caspol.exe), и аргумент strongname, определяющий, что мы хотим увидеть строгое имя. Введите эту команду, и появится листинг, содержащий открытый ключ устойчивого имени, имя сборки и версию сборки.

secutil.exe -hex -strongname securityapp10.exe

Microsoft (R) .NET Framework SecUtil 1.0.xxxx.x

Copyright (c) Microsoft Corp 1999-2001. All rights reserved.

Public Key =

0x002400000480000094000000060200000024000052534131000400000100010x7508D0780C56AF85BAl

BAD6P88E2C653E0A836286682C18134CC988546C1143252795A791F042238040F5627CCC1590ECE

A30A9CD4780F5F0B29B55C375D916A33FD46B14582836E346A316BA27CD555B8F715377422EF589770

E5A5346AOOBAABB70EF36774DFBCB17A30B67C913384E62A1C762CF40AFE6F1F605CCF406ECF

Name = SecurityApp10

Version = 1.0.513.28751

Success

Можно заметить, что по умолчанию устанавливаются два строгих имени групп кода. Одно из них является ключом устойчивого имени кода Microsoft, а второй ключ устойчивого имени предназначен для частей .NET, представленных для стандартизации в ЕСМА, над которыми Microsoft имеет значительно меньший контроль.

Распространение кода с помощью сертификатов

В последнем разделе было рассмотрено, как можно применять уникальное устойчивое (строгое) имя к сборке, чтобы системный администратор мог явно предоставить сборкам полномочия, которые соответствуют этому устойчивому имени с помощью группы доступа к коду. Этот метод управления политикой системы безопасности может быть весьма эффективным, но иногда необходимо работать на более высоком уровне, где администратор политики системы безопасности предоставляет полномочия на основе издателя программного обеспечения, а не каждого отдельного программного компонента.

Чтобы предоставить информацию об издателе программного обеспечения, мы используем цифровые сертификаты и подписываем сборки, чтобы потребители программного обеспечения могли проверить идентичность издателя программного обеспечения. В коммерческой среде мы получаем сертификаты от таких компаний как Verisign или Thawte.

Преимущество покупки сертификата от внешнего поставщика перед созданием своего собственного состоит в том, что он предоставляет высокие уровни надежности в своей идентичности; поставщик действует как надежная третья сторона. Однако в целях тестирования .NET включает утилиту командной строки, которую можно использовать для создания тестового сертификата. Процесс создания сертификатов и их использование для публикации программного обеспечения является сложным, но чтобы дать представление о том, что сюда вовлечено, мы разберем пример, не вдаваясь в подробности.

Представим, что мы являемся компанией ABC Corporation, и создадим сертификат для нашего программного продукта "ABC Suite". Прежде всего необходимо создать тестовый сертификат, для чего вводим команду:

makecert -sk ABC -n "CN=ABC Corporation" abccorptest.cer

Эта команда создает тестовый сертификат с именем "ABC Corporation" и сохраняет его в файле abccorptest.cer. Аргумент -sk ABC создает расположение контейнера ключа, который используется криптографией с открытым ключом.

Чтобы подписать сборку с помощью сертификата, применяется утилита signcode.exe с файлом сборки, содержащим манифест сборки. Часто простейшим способом подписать сборку является использование утилиты signcode.exe в ее режиме мастера. Для запуска мастера введите signcode.exe без параметров:

Если нажать кнопку Next, вам будет предложено определить, где находится файл, который надо подписать. Для сборки подписывается файл, содержащий манифест.

Если нажать кнопку Next и выбрать режим Custom в следующем окне, будет предложено определить сертификат, который должен использоваться для подписи сборки. Нажав Select from File и указав файл abccorptest.cer, увидите экран подтверждения:

Появится окно, которое запросит файл с закрытым ключом. Этот файл с ключом был создан утилитой makecert, поэтому мы можем выбрать параметры, как показано на следующем снимке экрана. Поставщик криптографической службы является приложением, которое реализует криптографические стандарты. Криптография с открытым ключом была рассмотрена в главе 10.

Затем задается ряд вопросов о способе выполнения подписи, включая это окно, которое просит определить алгоритм шифрования:

После этого мы определяем имя приложения и URL страницы Web, которая дает о нем дополнительную информацию.

Это по сути окончание процесса, последнее окно подтверждает данные сертификата и тот факт, что сборка была успешно подписана.

Поскольку наш исполняемый файл теперь подписан с помощью сертификата, получатель сборки имеет доступ к надежному свидетельству того, кто опубликовал программное обеспечение. Среда выполнения может проверить сертификат и сопоставить издателя сборки с группой кода с высоким уровнем доверия в отношении идентичности кода, так как надежная третья сторона подтверждает идентичность издателя.

Разберем подписанную сборку более подробно. Хотя мы используем тестовый сертификат, можно временно сконфигурировать .NET для использования тестовых сертификатов как вызывающих доверие, выпущенных надежной третьей стороной, используя утилиту setreg.exe, которая позволяет преобразовать открытый ключ и настройки сертификата в реестре. Если ввести следующую команду, машина будет сконфигурирована таким образом, чтобы доверять тестовому сертификату в корне, что дает нам более содержательную тестовую среду.

setreg.exe 1 true

Проверим нашу сборку и ее уровень надежности с помощью утилиты chktrust.exe:

chktrust.exe securityapp11.exe

Эта команда выведет окно:

Отметим, что chktrust.exe успешно подтвердила достоверность издателя программного обеспечения, используя сертификат, но напоминает, что, хотя сертификат был проверен, он является тестовым сертификатом.

Обратим теперь наше внимание к машине, которую мы хотим сконфигурировать для надежного программного обеспечения ABC Corporation. Для этого можно создать новую группу доступа к коду, которая соответствует программному обеспечению ABC Corporation. Итак, мы извлекаем шестнадцатеричное представление сертификата из сборки с помощью утилиты secutil.exe:

secutil.exe -hex -х securityapp11.exe

Эта команда выведет что-то подобное:

Microsoft (R) .NET Framework SecUtil 1.0.xxxx.x

Copyright (c) Microsoft Corp 1999-2001. All rights reserved.

X.509 Certificate =

0x3082017B30820125A0030201020210D69BE8D88D8FF9B54A9C689A71BB7E33300D06092A864886F7

0001010405003016311430120603550403130B526F6F74204167656E6379301E170D30313035323831

38333133305A170D3339313233313233353935395A301A311830160603550403130F41424320436F72

706F726174696F6E305C300D06092A864886F70D0101010500034B003048024100ECBEFB348C1364B0

A3AE14FA9805F893AD180C7B2E57ADABBBE7EF94694A1E92BC5B4B59EF76FBDAC8D04D3DF2140B7616

550FE2D5AE5F15E03CBB54932F5CBB0203010001A34B304930470603551D010440303E801012E4092D

061D1D4F008D6121DC166463A1183016311430120603550403130B526F6F74204167656E6379821006

376C00AA00648A11CFB8D4AA5C35F4300D06092A864886F70D010104050003410011D1B5F6FBB0C4C0E

85A9BB5FDA5FEC1B8D9C229BB0FBBA7CBE3340A527A5B25EAA2A70205DD71571607291272DA581981C

73028AB849FF273465FAEF2F4C7174

Success

Создадим новую группу кода и применим полномочие FullTrust к сборкам, опубликованным ABC Corporation, с помощью следующей команды (достаточно длинной):

caspol -addgroup 1. -pub -hex

3082017B30820125A0030201020210D69BE8D88D8FF9B54A9C689A71BB7E33300D06092A864886F70D

01010405003016311430120603550403130B526F6F742041678656E6379301E170D3031303532383138

333133305A170D3339313233313233353935395A301A311830160603550403130F41424320436F7270

6F726174696F6E305C300D06092A864886F70D0101010500034B003048024100ECBEFB348C1364BOA3

AE14FA9805F893AD180C7B2E57ADABBBE7EF94694A1E92BC5B4B59EF76FBDAC8D04D3DF2140B761655

0FE2D5AE5F15E03CBB54932F5CBB0203010001A34B304930470603551D010440303E801012E4092D06

1D1D4F008D6121DC166463A1183016311430120603550403130B526F6F74204167656E637982100637

6C00AA00648A11CFB8D4AA5C35F4300D06092A86

4886F70D01010405000341001D1B5F6FBB0C4C0E85A9BB5FDA5FEC1B8D9C229BB0FB8A7CBE3340A527

A5B25EAA2A70205DD71571607291272D5A81981C73028AB849FF273465FAEF2F4C7174 FullTrust

Параметры определяют, что группа кода должна добавляться к верхнему уровню (1.), условие членства в группе кода имеет тип Publisher, а последний параметр определяет множество полномочий для предоставления (FullTrust). Команда будет требовать подтверждения:

Microsoft (R) .NET Framework CasPol 1.0.xxxx.x

Copyright (c) Microsoft Corp 1999-2001. All rights reserved.

The operation you are performing will alter security policy.

Are you sure you want to perform this operations? (yes/no) у

Added union code group with "-pub" membership condition to the Machine level.

Success

Машина теперь сконфигурирована при наличии полного доверия всем сборкам, которые были подписаны с помощью сертификата ABC Corporation. Чтобы подтвердить это, выполним команду caspol.exe -lg, которая выводит новую группу доступа к коду (1.8):

Security is ON

Execution checking is ON

Policy change prompt is ON

Level = Machine

Code Groups:

1. All code: Nothing

1.1. Zone - MyComputer: Full Trust

1.1.1. Zone — Intranet: LocalIntranet

1.2.1. All code: Same site Socket and Web.

1.2.2. All code: Same directory FileIO - Read, PathDiscovery

1.3. Zone — Internet: Internet

1.3.1. All code: Same site Socket and Web.

1.4. Zone — Untrusted: Nothing

1.5. Zone — Trusted: Internet

1.5.1. All code: Same site Socket and Web.

1.6. StrongName - 0024000004800000940000000602000000240000525341310004000001

000190007DlFA57C4AED9F0A32E84AA0FAEFD0DE9E8FD6AEC8F87FB03766C834C99921EB23BE79AD9D5

DCC1DD9AD236132102900B723CF980957FC4E177108FC607774F29E8320E92EA05ECE4E821C0A5EFE8

F1645C4C0C93C1AB99285D622CAA652C1FAD63D745D6F2DF5F17E5EAF0FC4963D261C8A1143651820

6DC093344D5AD293: FullTrust

1.7. StrongName - 0000000000000000040000000000000: FullTrust

1.8. Publisher -

3048024100ECBEFB348C1364B0A3AE14FA9805F893AD180C7B2E57ADABBBE7EF94694A1E92BC5B4B59

EF76FBDAC8D04D3DF2140B7616550FE2D5AE5F1 5E03CBB54932F5CBB0203010001: FullTrust

Success

В качестве еще одной проверки попросим caspol.exe сообщить, какие группы кода соответствуют нашей сборке:

caspol.exe -resolvegroup securityapp11.exe

Level = Enterprise

Code Groups:

1. All code: FullTrust

Level = Machine

Code Groups:

1. All code: Nothing

1.1. Zone — Intranet: LocalIntranet

1.1.1. All code: Same site Socket and Web.

1.1.2. All code: Same directory FileIO — Read, PathDiscovery

1.2. Publisher - 3048024100ECBEFB348C1364B0A3AE14FA9805F893AD180C7B2E57ADABB

BE7EF94694A1E92BC5B4B59EF76FBDAC8D04D3DF2140B7616550FE2D5AE5F15E03CBB54932F5CBB0

203010001: FullTrust

Level = User

Code Groups:

1. All code: FullTrust

Success

Результаты работы утилиты показывают, что сборка получает множество полномочий FullTrust при соответствии новой группе кода.

Управление зонами

Мы уже говорили о зонах, предоставляемых Windows, которыми мы управляем с помощью инструментов безопасности из Internet Explorer. Вот эти четыре зоны:

□ Intranet — все сайты Web, которые не находятся в интранет текущей организации.

□ Trusted Sites — сайты Web, которые не способны разрушить данные пользователя.

□ Restricted Sites — сайты Web, которые потенциально могут повредить компьютер.

□ Internet — все сайты Web, не попавшие в другие зоны.

Эти настройки управляются из Internet Explorer, так как они применимы к сайтам, посещаемым с помощью браузера, имеющего доступ к коду .NET (либо загруженному, либо в элементах управления страниц). Если используется другой браузер, он скорее всего не будет поддерживать код .NET и поэтому не будет параметров для управления ассоциированными зонами.

Любой пользователь на машине может изменить настройки зон, однако, настройки системы безопасности для зон, которые они определяют, применимы только для его учетной записи, т. е. один пользователь не может изменить у другого пользователя настройки зон. Это говорит о существовании риска, что пользователь может изменить настройки зон, не понимая, что делает, и невольно открыть свою машину для атаки.

Чтобы изменить настройки, связанные с каждой зоной, откройте Internet Explorer, а затем диалоговое окно Options из меню Tools. В окне Options перейдите на вкладку Security:

В верхней части видны четыре зоны. Выберите одну из них, щелкнув мышью на ее значке. Используйте кнопку Sites для определения сайтов, которые желательно включить в эту зону. Например, для конфигурирования зоны LocalIntranet, воспользуйтесь следующим диалоговым окном.

Присутствующие здесь параметры предоставляют достаточно возможностей, чтобы аккуратно определить, что составляет 'интранет' в организации пользователя. Кроме того, кнопка Advanced позволяет открыть диалоговое окно, где задается URI для сайтов, которые мы хотим включить в зону LocalIntranet.

Отметим параметр в нижней части этого диалогового окна, который предоставляется для каждой из зон, за исключением зоны Internet. Он задает, что сайты в этой зоне будут считаться надежными, если соединение с ними происходит через защищенный HTTP с помощью шифрования Secure Sockets Layer(SSL). Если считать надежным сайт, который доступен через незашифрованное соединение, возникает потенциальный риск атаки, так как трафик может быть перехвачен. Если желательно проверить, находится ли сайт в определенной зоне, посетите сайт и посмотрите на нижний правый угол окна Internet Explorer, в котором будет выводиться имя зоны для текущего адреса Web.

Помимо возможности найти область действия зоны, указывая надежные и ненадежные сайты, можно также определить, какие действия допускаются в каждой зоне с помощью настроек уровня безопасности. В их компетенции находятся такие вещи, как предупреждение об элементах управления ActiveX и разрешение принимать cookie.

Заключение

В этой главе мы рассмотрели, как сборки входят в группы кода, как этим группам кода присваивают полномочия на основе политики безопасности на уровнях пользователя, предприятия и машины, и как можно использовать утилиты для управления этой политикой. Мы также увидели, что для реализации сборки должны быть соответствующие полномочия на трех уровнях политики, а также правильные полномочия на основе ролей и соответствующие полномочия учетных записей Windows. Кроме того, здесь описаны имеющиеся возможности для распространения кода с помощью устойчивых имен и цифровых сертификатов.

Легко видеть, что в .NET разработан более высокий уровень контроля безопасности, чем существовало раньше в Windows, при этом большая часть системы безопасности поставляется автоматически, так как не требуется никаких усилий, чтобы использовать ее на базовом уровне. И когда необходимо расширить ее, нам предоставляются классы и инфраструктуры.

Безопасность постоянно находится в поле зрения Microsoft, и хотя компания пока не решила все проблемы, управляемая система безопасности .NET, является существенным шагом вперед, так как предоставляет среду, в которой код проверяется прежде, чем начинается его выполнение.

Конечно, не случайно эти разработки происходят в то время, когда Microsoft все больше склоняется в сторону распространения своих продуктов через Web, так как безопасность при этом является существенным фактором.

Пpиложeние A

C# для разработчиков C++

Введение

Это приложение предназначено для разработчиков, которые уже хорошо знакомы с C++ и хотят узнать, какие существуют различия между C++ и C#. Мы сделаем обзор языка C#, отмечая специально те области, где он отличается от C++. Так как два языка имеют много общего в синтаксисе и методологии, хорошо подготовленные программисты C++ смогут использовать это приложение в качестве краткого курса C#.

Необходимо четко понимать, что C# является языком программирования, отличным от C++. В то время как C++ был создан для общего объектно-ориентированного программирования в те дни, когда типичный компьютер был автономной машиной, выполняющей интерфейс пользователя на основе командной строки, C# разработан специально для работы с .NET и согласован с современной средой Windows и управляемыми мышью интерфейсами пользователя, сетами и Интернетом, Однако также неоспоримо, что два языка очень похожи как своим синтаксисом, так и тем что оба они созданы для использования одной парадигмы программирования, где код основывается на иерархиях наследуемых классов. Эта похожесть неудивительна при условии, что, как часто отмечалось в этой книге, C# в большой степени был создан как объектно-ориентированный язык, взявший самое лучшее из ранее созданных объектно-ориентированных языков программирования, из которых C++, несомненно, был до сих пор наиболее успешным примером, но отказался от более неудачных свойств этих языков

В связи со сходством между этими двумя языками программирования разработчики, использующие C++, могут обнаружить, что самый простой путь изучения C# состоит в использовании его как C++ с небольшими отличиями и в изучении этих отличий. Это приложение создано для того, чтобы в этом помочь. Мы начнем с обширного обзора, который в общих терминах дает понятия об основных различиях между двумя языками и также указывает, какие области у них совпадают. Затем мы сравним как выглядит стандартная программа "Hello, World" в каждом из этих языков. Большой объем приложения посвящен последовательномy анализу каждой из основных областей языка и подробному сравнению C# и C++. Очевидно, что приложение такого объема не может быть исчерпывающе полным, но оно создано для того чтобы охватить главные различия между языками, которые могут встретиться в ходе повседневного программирования. Отметим, что C# в большом числе областей существенно опирается на поддержку библиотеки базовых классов платформы .NET. В этом приложении мы ограничим наше внимание самим языком C# и не будем подробно рассматривать базовые классы.

Для целей сопоставления в качестве эталона используется ANSI C++. Компания Microsoft добавила многочисленные расширения к C++, но компилятор Windows C++ имеет некоторые отличия, несовместимые со стандартом ANSI, которые будут указаны, хотя они обычно не используются при сравнении двух языков.

Соглашения в этом приложении

Отметим, что в данном приложении мы придерживаемся дополнительных соглашений при изображении кода. Код C# всегда выводится, как и в остальных частях книги, с серым затенением:

// это код C#

class MyClass : MyBaseClass {

Если требуется выделить новый или важный код C#, он будет выводиться жирным шрифтом:

// это код C#

class MyClass : MyBaseClass // мы уже видели этот фрагмент

{

int x; // это интересно

Код C++, представленный для сравнения, выглядит следующим образом:

// это код C++

class CMyClass : public CMyBaseClass {

В образцах кода в этом приложении при использовании двух языков под Windows учитываются также большинство общих соглашений о наименованиях. Следовательно, имена классов в примерах C++ начинаются с C, в то время как соответствующие имена в примерах C# — нет. Также часто в образцах кода C++ используется для имен переменных "венгерский" стиль именования объектов.

Терминология

Необходимо знать, что несколько конструкций языка используют различную терминологию в C# и C++. Переменные члены в C++ называются полями в C#, в то время как функции в C++ называются методами в C#. В C# термин функция имеет более общее значение и ссылается на любой член класса, который содержит код. Это означает, что функция охватывает методы, свойства, конструкторы, деструкторы, индексаторы и перезагруженные версии операторов. В C++ функция и метод часто используются взаимозаменяемо, хотя, строго говоря, в C++ метод является виртуальной функцией-членом.

Если все это звучит путано, то следующая таблица должна в этом помочь разобраться:

Значение

Термин C++

Термин C#

Переменная, которая является членом класса

Переменная-член

Поле

Любой элемент в классе, который содержит инструкции

Функция (или функция-член)

Функция

Элемент класса, который содержит инструкции и вызывается по имени с помощью синтаксиса DoSomething(/* параметры */)

Функция (или функция-член)

Метод

Виртуальная функция, которая определена как член класса

Метод

Виртуальный метод

Необходимо также знать о паре других различных терминов:

Термин C++

Термин C#

Составной оператор

Блочный оператор

lvalue

Переменное выражение

В этом приложении будет по возможности использоваться терминология, соответствующая рассматриваемому языку.

Сравнение C# и C++

В этом разделе мы кратко рассмотрим общие различия и сходства между двумя языками.

Различия

Основные области, в которых C# отливается от C++, представлены ниже:

□ Использование компиляции. Код C++ обычно компилируется в язык ассемблера. C#, наоборот, компилируется в промежуточный язык (IL, intermediate language), который имеет некоторые сходства с байт-кодом Java. IL потоп преобразуется в собственный исполнимый код процессом оперативной компиляции (JIT) Создаваемый код IL хранится в файле или множестве файлов, называемом сборкой. Сборка по сути формирует единицу, в которой упакован код IL. Эта единица соответствует DLL или исполнимому файлу, создаваемому компилятором C++.

□ Управление памятью. C# создан с целью освободить разработчика от проблем, связанных управлением памятью. Это означает, что в C# не требуется явно удалять память, которая была динамически выделена из кучи, как это требуется делать в C++. Вместо этого сборщик мусора периодически очищает память. Для облегчения процесса C# накладывает некоторые ограничения на то, как можно использовать переменные, которые хранятся в куче и более строгие ограничения имеет C++ в отношении безопасности типов данных.

□ Указатели. Указатели используются в C# также, как и в C++, но только в тех блоках кода, которые специально помечены для использования указателей. Большей частью C# полагается на ссылки в стиле VB/Java для экземпляров классов, и язык был создан с таким расчетом, чтобы указатели не требовались так часто, как в C++.

□ Перезагрузка операторов. C# не позволяет явно перезагружать так много операторов, как C++. Это по большей степени связано с тем, что компилятор C# в некоторой степени автоматизирует эту задачу, используя любые доступные специальные перезагружаемые версии элементарных операторов (таких как =) для автоматической реализации перезагруженных версий комбинированных операторов (+=).

□ Библиотека. Как C++, так и C# зависят от наличия обширной библиотеки. Для ANSI C++ это — стандартная библиотека, для C# — множество классов, называемых базовыми классами .NET. Базовые классы .NET основываются на одиночном наследовании, а стандартная библиотека — на комбинации наследования и шаблонов. В то время как ANSI C++ держит библиотеку большей частью отдельно от самого языка, в C# такая взаимная зависимость значительно сильнее, и реализация многих ключевых слов C# прямо зависит от определенных базовых классов.

□ Среды использования. C# специально разрабатывался для основных потребностей программирования в рабочих средах на основе GUI (не обязательно только Windows, хотя язык пока доступен только для Windows), а также в таких базовых службах, как службы Web. Это на самом деле не повлияло на сам язык, но отразилось в дизайне библиотеки базовых классов C++, в противоположность такому подходу, создавался для более общего использования, в те времена доминировали интерфейсы пользователя на основе командной строки. Ни C++, ни стандартная библиотека не включают никакой поддержки элементов GUI. В Windows paзработчики C++ прямо или косвенно используют для этого API Windows.

□ Директивы препроцессора. C# имеет несколько директив препроцессора, которые следуют тому же общему синтаксису, что и C++. Но в целом существует значительно меньшее количество препроцессорных директив в C#, так как другие свойства языка C# снижают их значение.

□ Перечислители. Присутствуют в C#, но действуют более гибко, чем их эквивалентны в C++, так как являются синтаксически полноценными самостоятельными структурами, поддерживающими различные свойства и методы. Отметим, что их поддержка существует только в исходном коде, при компиляции в собственный исполнимый код перечислители по-прежнему реализуются как примитивные числовые типы, поэтому потери производительности нет.

□ Деструкторы. C# не может гарантировать, когда вызовется деструктор класса. В целом нет необходимости использовать парадигму программирования для создания кода деструкторов класса C#. Это возможно в C++, если только нет таких специальных внешних ресурсов, как соединения с файлом или базой данных, которые необходимо очистить. Так как сборщик мусора очищает всю динамически выделяемую память, деструкторы не играют такой важной роли в C#, как в C++. Для тех случаев, когда важно очистить внешние ресурсы как можно скорее, C# реализует альтернативный механизм, включающий интерфейс IDisposable.

□ Классы и структуры. C# формализует различия между классами (используемыми обычно для больших объектов с множеством методов) и структурами (используемыми обычно для небольших объектов, которые содержат чуть больше, чем обычные совокупности переменных). Классы и структуры хранятся неодинаково, структуры не поддерживают наследование, и есть другие различия.

Сходства

Области, где C# и C++ очень похожи

□ Синтаксис. Весь синтаксис C# очень похож на синтаксис C++, хотя существуют многочисленные небольшие различия.

□ Поток выполнения. C++ и C# имеют приблизительно схожие инструкции управления потоком выполнения. Они обычно работают одинаково в обоих языках.

□ Исключения. Поддержка для них в C# по сути такая же как в C++, за исключением того, что C# допускает блоки finally и накладывает некоторые ограничения на тип объекта, который может порождаться.

□ Модель наследования. Классы наследуются одинаковым образом в C# и C++. Связанные концепции, такие как абстрактные классы и виртуальные функции, реализуются одинаковым образом, хотя и существуют некоторые различия в синтаксисе. C# поддерживает также только одиночное наследование классов. Сходство иерархии классов в связи с этим означает, что программы C# будут иметь общую архитектуру, очень похожую на соответствующие программы C++.

□ Конструкторы. Работают одинаковым образом в C# и C++, но опять же существуют некоторые различия в синтаксисе.

Новые свойства

C# вводит ряд новых концепций, которые не являются частью спецификации ANSI C++ (хотя большинство из них были введены компанией Microsoft как нестандартные расширения, поддерживаемые компилятором Microsoft C++). Они включают в себя следующие понятия:

□ Делегаты. C# не поддерживает указатели на функции. Однако аналогичный результат достигается помещением ссылок на методы в классах специальной формы, называемых делегатами. Делегаты могут передаваться между методами и использоваться для вызова методов, ссылки на которые они содержат, таким же образом, как указатели на функции могут использоваться в C++. В отношении делегатов важно отметить, что они содержат в себе объектные ссылки, также как и ссылки на методы. Это означает. что в отличие от указателей на функции, делегат содержит достаточно данных для вызова экземпляра метода в классе.

□ События. События аналогичны делегатам, но созданы специально для поддержки модели обратного вызова, в которой клиент уведомляет сервер о своем желании быть информированным, когда произойдет некоторое действие. C# использует события в качестве оболочек вокруг сообщений Windows таким же образом, как это делает VB.

□ Свойства. Эта идея, интенсивно используемая в VB и в COM, была импортирована в C#. Свойство является в классе методом или парой методов получения/задания, которые синтаксически оформлены так, что для внешнего мира они выглядят как поле. Они позволяют написать код в виде MyForm.Height = 400 вместо MyForm.SetHeight(400).

□ Интерфейсы. Интерфейс может рассматриваться как абстрактный класс, назначение которого состоит в определении множества методов или свойств, которые классы могут согласиться реализовать. Идея ведет свое происхождение из COM. Интерфейсы C# не такие, как интерфейсы COM — они являются просто списками методов и т.д., в то время как интерфейсы COM имеют другие связанные свойства, такие как GUID, но принцип очень похож. Это означает, что C# формально распознает принцип наследования интерфейса, посредством которого класс наследует определения функций, но без каких-либо реализаций.

□ Атрибуты. C# позволяет дополнить классы, методы, параметры и другие элементы в коде с помощью мета-информации, называемой атрибутами. Атрибуты доступны во время выполнения и используются для определения действий, принимаемых кодом.

Новые свойства базовых классов

Следующие свойства являются новыми в C# и не имеют аналогов в языке C++. Однако поддержка этих свойств идет почти полностью из базовых классов с небольшой или незначительной поддержкой самого синтаксиса языка C# и поэтому они не будут рассматриваться в этом приложении. Подробное описание дано в главе 7.

□ Организация поточной обработки. Язык C# включает некоторую поддержку синхронизации потоков выполнения с помощью оператора lock. (C++ не имеет встроенной поддержки для потоков выполнения и в случае необходимости вызывается соответствующая функциональность из библиотек кода.)

□ Отражение. C# позволяет коду динамически получать информацию об определениях классов в компилированных сборках (библиотеках и исполняемом коде). Можно на самом деле написать программу на C#, которая выводит информацию о классах и методах, из которых она создана.

Неподдерживаемые свойства

Следующие части языка C++ не имеют никакого эквивалента в C#:

□ Множественная реализация наследования в классах. Классы поддерживают множественное наследование только для интерфейсов.

□ Шаблоны. Они не являются частью языка C# в настоящее время, хотя компания Microsoft утверждает, что исследует возможность поддержки шаблонов в будущих версиях C#.

Пример Hello World

Написание приложения 'Hello World' в мире программирования уже стало почти привычным. Но сопоставление 'Hello World' в C++ и C# может оказаться достаточно поучительным для иллюстрации некоторых различий между двумя языками. При этом сравнении сделана попытка внести немного новизны (и продемонстрировать дополнительные свойства), выводя Hello World как в командной строке, так и окне сообщения. Также сделано небольшое изменение текста сообщения в версии C++. Версия C++ выглядит следующим образом:

#include <iostream>

# include <Windows.h>

using namespace std;

int main(int argc, char *argv) {

cout << "Goodbye, World!";

MessageBox(NULL, "Goodbuy, World!", MB_OK);

return 0;

}

А вот версия C#:

using System;

using System.Windows.Forms;

namespace Console1; {

class Class1 {

static int Main(string[] args) {

Console.WriteLine("Hello, World!");

MessageBox.Show("Hello, World!");

return 0;

}

}

}

Сравнение двух программ говорит, что синтаксис двух языков очень похож. В частности, блоки кода отмечены скобками {}, а точка с запятой используется в качестве ограничителя инструкций. Подобно C++, C# игнорирует все пробелы между инструкциями.

Мы разберем примеры строка за строкой, рассматривая предоставляемые свойства.

Инструкции #include

Версия C++ 'Hello World!' начинается с пары директив препроцессора для включения некоторых заголовочных файлов.

#include <iostream>

#include <Windows.h>

Они отсутствуют в версии C#, что иллюстрирует важный момент относительно того, как C# обращается к библиотекам. В C++ необходимо включать заголовочные файлы, чтобы компилятор смог распознать соответствующие символы в коде. Необходимо отдельно проинструктировать редактор связей для ссылки на библиотеки, что достигается с помощью параметров командной строки, передаваемых редактору. C# на самом деле не разделяет компиляцию и редактирование связей таким образом, как это делает C++. В C# все реализуется через параметры командной строки (и только тогда, когда происходит обращение к чему-то вне базовой библиотеки). Параметры позволят компилятору найти все определения классов, поэтому явные ссылки в исходном коде не нужны. Это в действительности значительно более простой способ, и после привыкания к модели C# версия C++, где все необходимо указывать дважды, начнет выглядеть достаточно странной и громоздкой.

Еще момент, который необходимо отметить, состоит в том, что из двух инструкций #include в приведенном выше коде C++, первая обращается к стандартной библиотеке ANSI (часть iostream стандартной библиотеки). Вторая к специальной библиотеке Windows и используется для того, чтобы можно было вывести окно сообщения. Код C++ под Windows часто должен обращаться к API Windows, так как стандарт ANSI не имеет никаких средств создания окон. В противоположность этому базовые классы .NET — эквивалент C# стандартной библиотеки шаблонов ANSI — включает средства создания окон, и здесь используются только базовые классы .NET. Код C# в данном случае не требует никаких нестандартных средств. (Хотя и спорная, эта точка зрения уравновешивается тем фактом, что 'стандарт' C# в настоящее время доступен только в Windows.)

Приведенный выше код C# оказался не содержащим никакой директивы #include, но это не значит, что любые препроцессорные директивы (даже и не #include) недоступны в C#, и не сохраняется синтаксис #.

Пространства имен

Программа C# Hello World начинается с объявления пространства имен, которое ограничивается фигурными скобками, чтобы включить всю программу. Пространства имен работают точно таким же образом в C#, как в C++, предоставляя способы удаления возможной неопределенности имен символов программе. Размещение элементов в пространстве имен необязательно в обоих языках, но в C# соглашение состоит в том, что все элементы должны быть в пространстве имен. Следовательно, в то время как вполне обычно видеть код C++, который не содержится в пространстве имен, крайне редко можно увидеть такой код в C#.

Следующая часть кода в версиях C# и C++ очень похожа, в обоих используется инструкция using для указания пространства имен, в котором должны искаться все символы. Единственное различие является синтаксическим: в C# применяется инструкция namespace, в то время как в C++ используется using namespace.

Многие разработчики C++ привыкли использовать старую библиотеку C++, что означает включения файла iostream.h, а не файла iostream, и в этом случае инструкция using namespace std является ненужной. Старая библиотека C++ официально опротестована и не будет больше поддерживаться версией Visual Studio 8 (версией, за которой последует Visual Studio.NET). Приведенный выше пример демонстрирует, как в действительности необходимо использовать библиотеку iostream в коде C++.

Точка входа: Main() и main()

Следующие элементы в примерах Hello World являются точками входа программ. В случае C++ это будет глобальная функция с именем main(). C# делает примерно то же самое, хотя в C# именем является Main(). Однако в то время как в C++ функция main() определена вне любого класса, версия C# определена как статический член класса. Это связано с тем, что C# требует, чтобы все функции и переменные были членами класса или структуры C# не допускает никаких элементов верхнего уровня в программе, за исключением классов и структур. В этом отношении C# может рассматриваться как язык, обеспечивающий более строгое следование объектно-ориентированной практике, чем это делает C++. Существенное использование глобальных и статических переменных и функций в коде C++ считается в любом случае плохой практикой программирования.

Конечно, требование, чтобы все было членом класса, приводит к вопросу о том, где должна находиться точка входа программы. Ответ состоит в том, что компилятор C# ищет статический член метод с именем Mаin(). Это может быть член любого класса в исходном коде, но только один класс должен иметь такой метод. (Если более одного класса определяем этот метод, необходимо использовать ключ компилятора, чтобы указать компилятору какой из них должен быть точкой входа программы.) Подобно своему эквиваленту в C++ Main() может возвращать либо void, либо int, хотя более распространено int. Также, подобно своему эквиваленту в C++, Main() получает такой же эквивалент аргументов либо множество произвольных параметров командной строки, переданных в программу как массив строк, либо не получает никаких параметров. Но как можно видеть из кода, строки определены в C# более интуитивно понятным образом, чем в C++. Каждый массив хранит число элементов, которое он содержит, а также сами элементы, поэтому нет необходимости передавать отдельно число строк в массиве в коде C#, как делает C# с помощью параметра argc.

Вывод сообщения

Наконец мы переходим к строкам, которые действительно выводят сообщение на консоль. а затем в окно сообщения. В обоих случаях эти строки кода используют вызов свойств из поддерживающих эти два языка библиотек. Архитектура классов в стандартной библиотеке очевидно очень отличается от архитектуры библиотеки базовых классов .NET, поэтому детали вызовов методов в этих примерах кода различны. В случае C# оба вызова делаются как вызовы статических методов на базовых классах, в то время как вывод окна сообщения в C++ должен использовать нестандартную функцию API Windows MessageBox(), которая не является объектно-ориентированной.

Базовые классы спроектированы интуитивно понятными, существенно более понятными, чем в стандартной библиотеке. Без какого-либо знания C# сразу становится ясно, что делает Console.WriteLine(). Если не знать, то трудно понять, что означает cout <<.

MessageBox.Show() получает меньше параметров, чем ее эквивалент C++ в этом примере, так как является перезагруженным. Доступны и другие перезагружаемые версии, которые получают дополнительные параметры.

Еще один момент, который легко можно пропустить: приведенный выше код показывает, что C# использует точку, т.е. символ вместо двух двоеточий :: для разрешения области действия. Console и MessageBox являются именами классов, а не экземплярами классов. Чтобы получить доступ к статическим членам классов, C# всегда требует синтаксис <ИмяКласса>.<ИмяЧлена>, в то время как C++ дает возможность выбора между <ИмяКласса>::<ИмяЧлена> и <ИмяЭкземпляра>.<ИмяЧлена> (если экземпляр класса существует и находится в области действия).

Сравнение свойств

Приведенный выше пример дает общее представление о различиях, которые будут показаны. Оставшуюся часть этого приложения мы посвятим детальному сравнению этих двух языков, периодически обращаясь к различным свойствам языков C++ и C#.

Архитектура программы

В этом разделе будет рассмотрен в очень общих терминах вопрос о том, как свойства двух языков влияют на общую архитектуру программ.

Программные объекты

В C++ любая программа состоит из точки входа (в ANSI C++ это функция main(), хотя для приложений Windows она обычно называется WinMain()), а также различных классов. структур и глобальных переменных или функций, которые определены вне любого класса. Хотя многие разработчики будут считать что хороший объектно-ориентированный проект определяется тем, насколько возможно, чтобы элементы самого верхнего уровня в коде являлись объектами C++ не требует этого. Как только что было показано, C# реализует эту идею. Он утверждает существенно более объектно-ориентированную парадигму, требуя, чтобы все элементы являлись членами класса. Другими словами, единственными объектами верхнего уровня в программе являются классы (или другие элементы, которые могут рассматриваться как специальные типы классов: перечисления, делегаты и интерфейсы). В этом случае код C# оказывается более объектно-ориентированным, чем это требует C++.

Файловая структура

В C++ синтаксис, на котором строится программа, основывается на понятии файла как единице исходного кода. Имеются, например, файлы исходного кода (файлы .срр), каждый из которых будет содержать директивы препроцессора #include для включения подходящих заголовочных файлов. Процесс компиляции содержит отдельную компиляцию каждого исходного файла, после чего эти файлы объектов компонуются для создания конечного исполнимого файла. Хотя конечный исполнимый файл не содержит никакой информации о первоначальных исходных или объектных файлах, C++ был создан таким образом, что разработчик явно кодирует в рамках выбранной структуры файлов исходного кода.

При использовании C# в ведении компилятора находятся детали соответствия отдельных файлов исходного кода. Можно поместить исходный код в один файл или в несколько файлов, но это не играет никакой роли для компилятора и нет необходимости для любого файла явно ссылаться на другие файлы. В частности, не существует требования определять элементы до ссылки на них в любом отдельном файле, как это требуется в C++. Компилятор успешно находит определение каждого элемента, где бы оно не находилось. Как побочный эффект этого, не существует в действительности никакой концепции компоновки кода в C#. Компилятор просто компилирует все исходные файлы в сборку (хотя можно определить другие варианты, к примеру, модуль — единица, которая будет формировать часть сборки). Компоновка не имеет места в C#, но она в действительности ограничивается компоновкой кода с любым существующим библиотечным кодом в сборках. В C# не существует такого понятия, как заголовочный файл.

Точка входа программы

В стандартном ANSI C++ точка входа программы является по умолчанию функцией с именем main(), которая имеет сигнатуру:

int main(int argc, char *argv)

Здесь argc указывает число аргументов, передаваемых в программу, a argv является массивом строк, задающих эти аргументы. Первый аргумент всегда является командой, используемой для выполнения самой программы. Windows несколько изменяет это. Приложения Windows традиционно начинаются с точки входа, называемой WinMain(), a DLL с DllMain(). Эти методы также получают другие множества параметров.

В C# точка входа следует аналогичным принципам. Однако в связи с требованием, что все элементы C# являются частью класса, точка входа не может быть глобальной функцией. Вместо этого, как было сказано ранее, требуется, чтобы один из классов имел статический метод-член с именем Main().

Синтаксис языка

C# и C++ используют практически идентичный синтаксис. Оба языка, например, игнорируют пробелы между инструкциями и используют точку с запятой для разделения инструкций и фигурные скобки для объединения инструкций в блоки. Все это означает, что на первый взгляд программы, написанные любым языком, выглядят очень похожими. Отметим, однако, следующие различия:

□ C++ требует точку с запятой после определения класса. C# не требует.

□ C++ позволяет использовать выражения как инструкции, даже если они не имеют результата, например:

i + 1;

В C# это будет ошибкой.

Необходимо также отметить, что подобно C++, C# различает строчные и заглавные символы. Однако, так как C# создан для взаимодействия с VB.NET (который не отличает такие символы), строго рекомендуется не использовать имена, которые разнятся только регистром символов для каких-либо объектов, видных коду вне данного проекта (другими словами, имена открытых членов классов в коде библиотеки). Если используются открытые имена, которые отличаются только регистром символов, то это не позволит коду VB.NET получить доступ к этим классам. (В случае написания какого-либо управляемого кода C++ для среды .NET применимы те же рекомендации.)

Опережающие объявления

Опережающие объявления не поддерживаются и не требуются в C#, так как порядок, в котором элементы определены в файлах исходного кода, не имеет значения. Вполне допустимо одному элементу ссылаться на другой элемент, который позже определяется в этом файле или в другом файле, он должен только где-то быть определен. Это противоположно C++, в котором на символы и т.д. можно ссылаться в любом из файлов исходного кода, если они уже были объявлены в том же файле или во включаемом файле.

Отсутствие разделения определения и объявления

С отсутствием опережающих объявлений в C# связано то, что в C# не существует никакого разделения объявления и определения для любых элементов. Например, в C++ принято описывать класс в заголовочном файле сигнатурой функций членов, а полные определения даны в другом месте:

class CMyClass {

public:

void MyMethod(); // определение этой функции находится в файле C++,

// если только MyMethod() не является встраиваемой функцией

// и т.д.

В C# этого не делают. Методы всегда определяются полностью в определении класса

class MyClass {

public void MyMethod() {

// здесь реализация

На первый взгляд может показаться, что это ведет к коду, который труднее читать. Достоинство подхода C++ в этом вопросе в конце концов состоит в том что можно просто просмотреть заголовочный файл, чтобы узнать, какие открытые функции предоставляет класс, не обращаясь к реализации этих функций. Однако это больше не требуется в C#, частично в связи с использованием современных редакторов (редактор Visual Studio.NET позволяет сворачивать реализации методов), а частично в связи с тем, что C# имеет средство автоматического создания документации для кода в формате XML.

Поток выполнения программы

Поток выполнения программы в C# аналогичен потоку C++. В частности, следующие инструкции работают точно таким же образом в C#, как они работают в C++, и имеют точно такой же синтаксис:

□ for

□ return

□ goto

□ break

□ continue

Существует пара синтаксических различий для инструкций if, while, do…while и switch, и C# предоставляет дополнительную инструкцию управления потоком выполнения foreach.

if…else

Инструкция if работает точно таким же образом и имеет такой же синтаксис в C#, как и в C++, кроме одного момента. Условие в каждом предложении if или else должно оцениваться как bool. Например, предположим что х является целым типом данных, а не bool, тогда следующий код C++ будет создавать ошибку компиляции в C#:

if (х) {

Правильный синтаксис C# выглядит так:

if (x != 0) {

так как оператор != возвращает bool.

Это требование является хорошей иллюстрацией того, как дополнительная безопасность типов в C# заранее перехватывает ошибки. Ошибки времени выполнения в C++, вызываемые написанием if (a=b), когда предполагалось написать if (a==b) являются достаточно распространенными. В C# эти ошибки будет перехватываться во время компиляции.

Отметим, что в C# невозможно преобразовать числовые переменные в или из bool.

while и do…while

Также, как и для if, эти инструкции имеют точно такой же синтаксис и назначение в C#, как и в C++, за исключением того, что условное выражение должно оцениваться как bool.

int X;

while (X) {/* инструкции */} // неправильно

while (X != 0) {/* инструкции */} // правильно

switch

Инструкция switch служит для тех же целей в C#, что и в C++. Она является, однако, более мощной в C#, так как используется строка в качестве проверяемой переменной, что невозможно в C++:

string MyString; // инициализировать MyString

switch (MyString) {

case "Hello":

// что-нибудь сделать

break;

case "Goodbye":

// и т.д.

Синтаксис в C# слегка отличается тем, что каждое предложение case должно явно заканчиваться. Не разрешается одному case содержать другой case, если только первый case не является пустым. Если желательно получить такой результат, используйте инструкцию goto.

switch (MyString) {

case "Hello":

// что-нибудь сделать

goto case "Goodbye"; // перейдет к выполнению инструкций

// в предложении "Goodbye"

case "Goodbye":

// сделать что-то еще

break;

case "Black": // здесь можно провалиться, так как он пустой

case "White":

// сделать что-то еще

// выполняется, если MyString содержит

// либо "Black", либо "White"

break;

default:

int j = 3;

break;

}

Компания Microsoft решила использовать инструкцию goto в этом контексте, чтобы предотвратить появление ошибок в случае, если требовалось выполнить пропущенный break, и код в инструкции switch проваливался в следующее предложение case.

foreach

C# предоставляет дополнительную инструкцию управления потоком выполнения foreach. foreach делает цикл по всем элементам массива или коллекции, не требуя явной спецификации индексов. Цикл foreach на массиве может выглядеть следующим образом. В этом примере предполагается, что MyArray является массивом double, и необходимо вывести каждое значение в консольном окне. Чтобы сделать это, используем следующий код:

foreach (double SomeElement in MyArray) {

Console.WriteLine(SomeElement);

}

Отметим что в этом цикле SomeElement является именем, присвоенным переменной для итерации по циклу; здесь можно выбрать любое имя не конфликтующее с другими именами переменных.

Запишем также приведенный выше цикл следующим образом:

foreach (double SomeElement in MyArray)

Console.WriteLine(SomeElement);

так как блочные инструкции в C# работают таким же образом, как составные инструкции в C++.

Этот цикл будет иметь точно такой же результат, как и следующий:

for (int I=0; I < MyArray.Length; I++) {

Console.WriteLine(MyArray[i]);

}

(Отметим, что вторая версия иллюстрирует также, как получить число элементов в массиве в C#. Мы рассмотрим, как массив объявляется в C#, позже.)

Отметим, однако, что в отличие от доступа к элементам массива, цикл foreach предоставляет к своим элементам доступ только для чтения. Следовательно, следующий код не будет компилироваться.

foreach (double SomeElement in MyArray)

SomeElement *= 2; // Неверно, для SomeElement нельзя выполнить

// присваивание

Мы упомянули, что цикл foreach может использоваться для массивов или коллекций. Коллекция не имеет аналога в C++, хотя концепция стала общераспространённой в Windows благодаря ее использованию в VB и COM. Коллекция по сути является классом, который реализует интерфейс IEnumerable. Так как это включает поддержку из базовых классов, понятие коллекция объясняется в главе 7.

Переменные

Определения переменных следуют в основном тем же образцам в C#, что и в C++:

int NCustomers, Result;

double DistanceTravelled;

double Height = 3.75;

const decimal Balance = 344.56M;

Хотя, как можно было ожидать, некоторые из типов различны. Как было замечено ранее, переменные могут быть объявлены только локально в методе или как члены класса. C# не имеет эквивалент, глобальных или статических (то есть с областью действия, ограниченной файлом) переменных в C++. Как уже говорилось, переменные, являющиеся членами класса, называются в C# полями.

Отметим, что C# также строго различаем типы данных, хранимые в стеке (типы данных значений) и хранимые в куче (ссылочные типы данных). Мы позже рассмотрим этот вопрос более подробно.

Базовые типы данных

Как и в C++, C# имеет ряд предопределенные типов данных, и можно определять собственные типы данных, такие как классы или структуры.

В C# и C++ предопределенные типы данных несколько различаются. Типы данных для C# приведены в таблице:

Имя

Содержит

Символ

sbyte

8-битовое целое число со знаком

byte

8-битовое целое число без знака

short

16-битовое целое число со знаком

ushort

16-битовое целое число без знака

int

32-битовое целое число со знаком

uint

32-битовое целое число без знака

U

long

64-битовое целое число со знаком

L

ulong

64-битовое целое число без знака

UL

float

32-битовое значение с плавающей точкой со знаком

F

double

64-битовое значение с плавающей точкой со знаком

D

bool

true или false

char

16-битовый символ Unicode

''

decimal

Число с плавающей точкой с 28 значащими цифрами

M

string

Множество символов Unicode переменной длины

""

object

Используется там, где не определен тип данных. Ближайшим эквивалентом в C++ является void*, за исключением того, что object не является указателем.

В приведенной выше таблице символ в третьем столбце указывает букву, которая может быть помещена после числа, чтобы указать его тип явно, например, 28UL означает число 28, хранимое как long без знака. Как и в случае C++, одиночные кавычки используются для обозначения символов, двойные кавычки для строк. Однако в C# символы всегда являются символами Unicode, а строки являются определенным ссылочным типом, а не просто массивом символов.

Типы данных в C# используются более аккуратно, чем в C++. Например, в C++ обычно ожидается, что int будет занимать 2 байта (16 битов), но определение ANSI C++ разрешает, чтобы это зависело от платформы. Следовательно, в Windows int в C++ занимает 4 байта, столько же сколько и long. Это очевидно вызывает достаточно много проблем совместимости при переносе программ C++ между платформами. С другой стороны, в C# каждый предопределенный тип данных (за исключением string и object) имеет явное определение занимаемой памяти.

Так как размер каждого из примитивных типов (примитивным типом является любой из приведенных выше, за исключением string и object) фиксирован в C#, то существует меньшая потребность в операторе sizeof, хотя он и есть в C#, но допустим только в ненадежном коде (как будет описано позже).

Несмотря на то, что многие имена в C# аналогичны именам C++ и существует достаточно интуитивно понятное отображение между многими из соответствующих типов, некоторые вещи отличаются синтаксически. В частности, signed и unsigned не являются ключевыми словами в C# (в C++ можно использовать эти ключевые слова, также как long и short для модификации других типов данных (например, unsigned long, short int). Такие модификации недопустимы в C#, поэтому приведенная выше таблица является фактически полным списком предопределенных типов данных.

Базовые типы данных как объекты

В отличие от C++ (но как в Java) базовые типы данных в C# трактуются как объекты, чтобы вызывать на них некоторые методы. Например, в C# возможно преобразование целого числа в строку следующим образом.

int I = 10;

string Y = I.ToString();

Можно даже написать:

string Y = 10.ToString();

Тот факт, что базовые типы данных рассматриваются как объекты, показывает тесную связь между C# и библиотекой базовых классов .NET. C# компилирует базовые типы данных, отображая каждый из них в один из базовых классов, например, string отображается в System.String, int в System.Int32 и т.д. Поэтому на самом деле в C# все является объектом. Однако отметим, что это применимо только для синтаксических целей. В реальности при выполнении кода эти типы реализуются как описанные ниже типы промежуточного языка, поэтому нет потери производительности, связанной с интерпретацией базовых типов как объектов. Здесь не будут перечисляться все методы, доступные для базовых типов данных, так как подробности представлены в MSDN. Однако необходимо отметить следующие особенности:

□ Все типы имеют метод ToString(). Для базовых типов данных он возвращает строковое представление их значения.

□ char содержит большое число свойств, которые предоставляют информацию о своем содержимом (IsLetter, IsNumber и т.д.), а также методы для выполнения преобразований (ToUpper(), ToLower()).

□ string имеет очень большое число методов и свойств. Строки будут рассмотрены отдельно.

Также доступен ряд статических методов членов и свойств. Они включают следующие:

□ Целые типы имеют MinValue и MaxValue, чтобы указать минимальное и максимальное значения, которые могут содержаться в типе данных.

□ Типы данных float и double также имеют свойство Epsilon, которое указывает наименьшее возможное значение больше нуля, которое может в нем содержаться.

□ Отдельные значения — NaN (не число, которое не определено), PositiveInfinity (положительная бесконечность) и NegativeInfinity (отрицательная бесконечность) определены для float и double. Результаты вычисления будут возвращать эти значения в подходящих ситуациях, например, деление положительного числа на ноль будет иметь в результате PositiveInfinity, в то время как деление нуля на нуль создаст NaN. Эти значения доступны как статические свойства.

□ Многие типы, включая все числовые, имеют статический метод Parse(), который позволяет преобразование из строки: double D = double.Parse("20.5").

Отметим, что статические методы в C# вызываются определением имени типа данных: int.MaxValue и float.Epsilon.

Преобразования базовых типов данных

Преобразование типа является процессом, в котором значение, хранящееся в переменной одного типа данных преобразуется в значение другого типа данных. В C++ это можно сделать явно или неявно:

float f1 = 40.0;

long l1 = f1; // неявно

short s1 = (short)l1; // явно, старый стиль C

short s2 = short(f1); // явно, новый стиль C++

Если преобразование типа определяется явно, то это означает, что в коде явно указывается имя типа данных назначения. C++ позволяет написать явные преобразования типа любым из двух стилей — старым стилем С, в котором имя типа данных помещалось в скобки, или новым стилем, в котором имя переменной помещается в скобки. Оба стиля показаны выше и являются вопросом синтаксического предпочтения, выбор стиля не оказывает никакого влияния на код. В C++ допустимы преобразования любых базовых типов данных. Однако, если существует риск потери данных в связи с тем, что тип данных назначения имеет меньший диапазон значений, чем исходный тип данных, то компилятор может послать предупреждение в зависимости от настройки уровня предупреждений. В приведенном выше примере неявное преобразование типа может вызвать потерю данных, поэтому компилятор будет обычно порождать предупреждение. Явное определение преобразования является на самом деле способом сообщить компилятору что данное действие обдуманно, в результате это обычно приводит к подавлению всех предупреждений.

Так как C# создан с целью обеспечить большую безопасность типов, чем C++, он менее гибок в отношении преобразований между типами данных, Он также формализует понятие явного и неявного преобразования типов данных. Некоторые преобразования определены как неявные, что позволяет выполнить их либо с помощью неявного, либо явного синтаксиса. Другие можно делать только с помощью явного преобразования типов, и компилятор будет давать ошибку (а не предупреждение, как в C++), если попробовать выполнить его неявно.

Правила в C#, имеющие отношение к тому, какие базовые числовые типы данных могут быть преобразованы в другие типы данных, вполне логичны. Неявными преобразованиями будут преобразования, которые не создают риск потери данных, например, int в long или float в double. Явными преобразованиями являются такие, где может быть потеря данных в связи с ошибкой переполнения, ошибкой знака или потерей дробной части числа, например, float в int, int в uint или short в ulong. Кроме того, так как char рассматривается несколько отдельно от других целых типов данных, можно преобразовывать только явно в или из char.

Следующие выражения считаются допустимыми в коде C#:

float f1 = 40.0F;

long l1 = (long)f1; // явное, так как возможна ошибка округления

short s1 = (short)l1; // явное, так как возможна ошибка переполнения

int i1 = s1; // неявное — никаких проблем

uint i2 = (uint)i1; // явное, так как возможна ошибка знака

Отметим, что в C# явное преобразование типов данных всегда делается с помощью старого синтаксиса в стиле C. Новый синтаксис C++ использовать нельзя.

uint i2 = uint(i1); // неверный синтаксис - это не будет компилироваться

Проверяемое (checked) преобразование типов данных

C# предлагает возможность выполнять преобразования типов и другие арифметические операции в проверяемом (checked) контексте. Это означает, что среда выполнения .NET будет обнаруживать возникновение переполнения и порождать исключение (конкретно, OverFlowException). Это свойство не имеет аналога в C++.

checked {

int I1 = -3;

uint I2 = (uint)I1;

}

В связи с контролируемостью контекста вторая строка будет порождать исключение. Если не определить checked, исключения не возникнет и переменная I2 будет содержать мусор.

Строки

Обработка строк выполняется значительно легче в C#, чем это было раньше в C++. Это связано с понятием строки как базового типа данных, который распознается компилятором C#. В C# нет необходимости рассматривать строки как массивы символов.

Ближайшим эквивалентом для типа данных string в C# является класс string в C++ в стандартной библиотеке. Однако строка C# отличается от строки C++ следующими основными свойствами.

□ Строка C# содержит символы Unicode, а не ANSI.

□ Строка C# имеет значительно больше методов и свойств, чем версия в C++.

□ Класс string стандартной библиотеки C++ является не более чем классом, предоставленным библиотекой, в то время как в C# синтаксис языка специально поддерживает класс string как часть языка.

Последовательности кодирования

C# использует тот же метод кодирования специальных символов, что и C++,— с помощью обратной наклонной черты. Вот список кодирования:

Последовательность

Имя символа

Кодировка Unicode

\'

Одиночная кавычка

0x0027

\"

Двойная кавычка

0x0022

\\

Обратный слэш

0х005C

\0

Null

0x0000

\a

Сигнал

0x0007

\b

Возврат на одну позицию

0x0008

\f

Перевод страницы

0x000C

\n

Новая строка

0x000A

\r

Возврат каретки

0x000D

\t

Горизонтальная табуляция

0x0009

\v

Вертикальная табуляция

0x000B

Это по сути означает, что в C# используются те же коды, что и в C++, за исключением того, что C# не распознает \?.

Имеются два отличия между символами кодирования в C++ и C#:

□ Последовательность кодирования \0 распознается в C#. Однако она не используется как терминатор строки в C# и поэтому может встраиваться в строку. Строки C# работают, сохраняя отдельно свои длины, поэтому никакой символ не используется в качестве терминатора. Поэтому строки C# в действительности могут содержать любой символ Unicode.

□ C# имеет дополнительную последовательность кодирования \uxxxx (или эквивалентно \Uxxxx), где xxxx представляет 4-символьное шестнадцатеричное число, \uxxxx представляет символ Unicode xxxx, например, \u0065 представляет 'е'. Однако в отличие от других последовательностей кодирования \uxxxx может использоваться в именах переменных, а также в символьных и строковых константах. Например, следующий код допустим в C#.

int R\u0065sult; // тот же результат, что и int Result;

Result = 10;

Согласно документации последовательность кодирования не зависит от регистра символов: \uxxxx и \Uxxxx будут эквивалентны. Однако при написании этой книги обнаружилось, что только версия нижнего регистра успешно компилируется текущей версией .NET.

C# имеет также альтернативный метод представления строк, который более удобен для строк, содержащих специальные символы: размещение символа @ в начале строки избавляет все символы от кодирования. Эти строки называются дословными строками. Например, чтобы представить строку C:\Book\Chapter2 можно написать либо "C:\\Book\\Chaptеr2", либо @"C:\Book\Chapter2". Интересно, что это означает также, что можно включать символы возврата каретки в дословные строки без кодирования:

string Message = @"Это будет на первой строке,

а это будет на следующей строке"

Типы значений и ссылочные типы

C# разделяет все типы данных на две разновидности: типы значений и ссылочные типы. Это различие не имеет эквивалента в C++, где переменные всегда неявно содержат значения, если только переменная специально не объявлена как ссылка на другую переменную.

В C# тип значения действительно содержит свое значение. Все предопределенные типы данных в C# являются типами значений, за исключением object и string. Если определить свои собственные структуры и перечисления, они также будут типами значений. Это означает, что простые типы данных в C# обычно действуют точно таким же образом как в C++, когда им присваивают значения.

int I = 10;

long J = I; // создаёт копию значения 10

I = 15; //не влияет на J

Ссылочный тип, как предполагает его имя, содержит только ссылку на то место в памяти, где хранятся данные. Синтаксически он действует таким же образом как ссылки в C++, но в терминах того, что происходит реально, ссылки C# ближе к указателям C++. В C# object и string являются ссылочными типами, как и любые определенные самостоятельно классы. Ссылки C# могут быть переназначены для указания на другие элементы данных, по большей части таким же образом, как можно переназначить указатели C++. Также ссылкам C# можно присваивать значение null для указания, что они ни на что не ссылаются. Например, возьмем класс с именем MyClass, который имеет открытое свойство Width.

MyClass My1 = new MyClass(); // в C# new просто вызывает конструктор

My1.Width = 20;

MyClass My2 = My1; // My2 указывает теперь на то же место

// в памяти, что и My1

Му2.Width = 30; // Теперь My1.Width = 30, так как My1

// и Му2 указывают на одно место в памяти

My2 = null; // Теперь My2 не ссылается ни на что,

// My1 по прежнему ссылается на тот же объект

В C# невозможно программным путем объявить определенную переменную как тип значения или как ссылочный тип, это определяется исключительно типом данных переменной.

Тип значения и ссылочный тип данных имеют особенности в управлении памятью, так как ссылочные типы всегда хранятся в куче, в то время как типы значений обычно хранятся в стеке. Это рассматривается более подробно в следующем разделе об управлении памятью.

Инициализация переменных

В C++ переменные никогда не инициализируются, если их явно не инициализировать (или в случае классов предоставить конструкторы). Если этого не сделать, переменные будут содержать какие-то случайные данные, оказавшиеся в памяти, это отражает особое внимание в C++ к производительности. C# уделяет больше внимания исключению ошибок во время выполнения и поэтому строже относится к инициализации переменных. В C# существуют следующие правила:

□ Переменные, которые являются полями-членами, по умолчанию инициализируются с помощью нулевых значений, если они не инициализируются явно. Это означает, что числовые типы данных будут содержать нули, bool будут содержать false, а все ссылочные типы (включая строки и объекты) будут содержать ссылку null. Структуры инициализируют нулем каждый свой член.

□ Локальные переменные методов не инициализируются по умолчанию. Однако компилятор будет давать ошибку, если локальная переменная используется до инициализации. Можно при желании инициализировать переменную, вызывая ее конструктор по умолчанию (тот, который обнуляет память).

// локальные переменные метода

int X1; //в этом месте X1 содержит случайные данные

// int Y = X1; // эта закомментированная строка будет создавать ошибку

// компиляции, т.к. X1 используется до инициализации

X1 = new int(); // теперь X1 будет содержать ноль.

Упаковка

В некоторых случаях необходимо использовать тип значения, как если бы он был ссылочным типом. Это можно сделать с помощью процесса, называемого упаковкой (boxing). Синтаксически упаковка означает просто преобразование переменной в объект.

int J = 10;

object BoxedJ = (object)J;

Упаковка действует как любое другое преобразование типов, но надо знать, что содержимое переменной скопируется в кучу и будет создана ссылка (так как объект BoxedJ является ссылочным типом).

Обычная причина для использования упаковки значения состоит в передаче его в метод, который ожидает в качестве параметра ссылочный тип. Можно также распаковать упакованное значение, преобразуя его просто назад в первоначальный тип данных.

int J = 10;

object BoxedJ = (object)J;

int K = (int)BoxedJ;

Отметим, что процесс распаковки будет инициировать исключение, если попытаться преобразовать значение к неправильному типу данных.

Управление памятью

В C++ переменные (включая экземпляры классов или структур) могут храниться в стеке или в куче. Обычно переменная хранится в куче, если она или некоторый содержащий ее класс был распределен с помощью оператора new, или в противном случае помещается в стек. Это означает, что возможность выделить динамически память для переменной с помощью оператора new позволяет выбирать, будет ли переменная храниться в стеке либо в куче. (Хотя очевидно в связи со способом работы стека, что хранящиеся в стеке данные будут существовать до тех пор, пока соответствующая переменная находится в области действия.)

C# работает совершенно по-другому. Чтобы понять как именно, рассмотрим два обычных сценария в C++. Возьмем следующее объявление двух переменных в C++:

int j = 30;

CMyClass *pMine = new CMyClass;

Здесь содержимое j хранится в стеке. Это в точности та ситуация, которая существует с типами данных значений C#. Экземпляр MyClass хранится, однако, в куче, а указатель на него находится в стеке, что по сути повторяет ситуацию со ссылочными типами в C#, за исключением того, что в C# синтаксис скрывает указатель под личиной ссылки. Эквивалент в C# будет следующим:

int J = 30;

MyClass Mine = new MyClass();

Этот код имеет в большой степени тот же результат, в соответствующих терминах как и приведенный выше код C++: различие состоит в том, что MyClass синтаксически используется как ссылка, а не как указатель.

Однако C++ и C# разнятся еще и тем, что C# не позволяет выбирать, как выделить память для определенного экземпляра. Например, в C++ можно сделать следующее действие:

int* рj = new int(30);

CMyClass Mine;

Это приведет к тому, что int будет находиться в куче, а экземпляр CMyClass — в стеке. Этого нельзя сделать в C#, так как C# считает, что int является типом значения, в то время как любой класс всегда будет ссылочным типом.

Другое различие состоит в том, что в C# не существует эквивалента оператора C++ delete. Вместо этого в C# сборщик мусора платформы .NET периодически вызывается и сканирует ссылки в коде, чтобы идентифицировать, какие области кучи в настоящее время используются программой. Затем он автоматически может удалить все объекты, которые больше не используются. Эта техника эффективно помогает избавиться от необходимости самостоятельно освобождать память в куче.

В C# следующие типы данных всегда являются типами значений:

□ Все простые предопределенные типы (за исключением object и string)

□ Все структуры

□ Все перечисления

Следующие типы данных всегда являются ссылочными типами:

□ object

□ string

□ Все классы

Оператор new

Оператор new имеет совершенно другое значение в C#, чем в C++. В C++ new указывает на запрос памяти из кучи. В C# new означает просто, что вызывается конструктор переменной. Однако действие аналогично в той степени, что если переменная имеет ссылочный тип то вызов ее конструктора будет неявно означать, что память выделяется в куче. Например, предположим, что имеется класс MyClass и структура MyStruct. В соответствии с правилами C# экземпляры MyClass всегда будут храниться в куче, а экземпляры MyStruct в стеке.

MyClass Mine; // Просто объявляем ссылку. Аналогично объявлению

// неинициализированного указателя в C++

Mine = new MyClass(); // создает экземпляр MyClass. Вызывает

// конструктор без параметров, в процессе этого

// выделяет память в куче

MyStruct Struct; // создает экземпляр MyStruct, но не вызывает

// никакого конструкторе. Поля в MyStruct

// будут неинициализированы

Struct = new MyStruct(); // вызывает конструктор, поэтому

// инициализирует поля, но не выделяет

// никакой памяти, так как Struct уже

// существует в стеке

Можно использовать new для того, чтобы вызвать конструктор для предопределенных типов данных:

int X = new int();

Это имеет такой же результат, как:

int X = 0;

Отметим, что это то же самое, что и

int X;

Последняя инструкция оставит X неинициализированной (если переменная X является локальной переменной метода).

Методы

Методы в C# определяются таким же образом, как функции в C++, с учетом факта, что методы C# всегда должны быть членами класса, и определение и объявление в C# всегда объединены:

class MyClass {

public int MyMethod() {

// реализация

Есть одно ограничение, состоящее в том, что методы-члены не могут объявляться как const в C#. Свойство C++ явно объявлять методы как const (другими словами, не изменяющими содержащий их экземпляр класса) выглядело первоначально как хорошее средство проверки на наличие ошибок во время компиляции, но оказалось вызывающим проблемы на практике. Это было связано с тем, что методы, чтобы сохранять открытое состояние класса, изменяют значения закрытых переменных членов, например, переменных, которые задаются при первом обращении. В коде C++ вполне можно встретить оператор const_cast, используемый для того, чтобы обойти метод, объявленный как const. В связи с этими проблемами компания Microsoft решила не использовать константные методы в C#.

Параметры методов

Как и в C++, по умолчанию параметры передаются в методы по значению. Если требуется это изменить, можно использовать ключевое слово ref, указывающее, что параметр передается по ссылке, и out, чтобы указать, что это параметр вывода (всегда передается по ссылке). Если это сделано, то необходимо объявлять этот факт как в определении метода, так и при его вызове.

public void MultiplyByTwo(ref double d, out double square) {

d *= 2;

square = d*d;

}

// позже, при вызове метода

double Value, Square Value = 4.0;

MultiplyByTwo(ref Value, out Square);

Передача по ссылке означает, что метод может изменять значение параметра. Передача по ссылке также осуществляется, чтобы улучшить производительность при работе с большими структурами, также как и в C++, передача по ссылке означает, что копируется только адрес. Отметим, однако, что, если при передаче по ссылке из соображений производительности вызываемый метод по-прежнему не изменяет значения параметра, то C# не разрешает присоединять модификатор const к параметрам, как это делает C++.

Параметры типа out действуют по большей части так же, как ссылочные параметры. Но они предназначены для случаев, когда вызываемый метод задает значение для параметра, а не изменяет его. Следовательно, инициализации параметров будут отличаться. C# требует, чтобы параметр ref инициализировался внутри вызываемого метода до своего использования.

Перезагрузка методов

Методы могут быть перезагружены таким же образом, как в C++. Однако C# не допускает в методах параметров по умолчанию. Это можно смоделировать с помощью перезагружаемой версии:

Для C++ можно сделать следующую запись:

double DoSomething(int someData, bool Condition=true) {

// и т.д.

В то время как в C# необходимо выполнить такие действия:

double DoSomething(int someData) {

DoSomething(someData, true);

}

double DoSomething(int someData, bool condition) {

// и т.д.

Свойства

Свойства не имеют эквивалента в ANSI C++, хотя они были введены как расширение в Microsoft Visual C++. Свойство является методом или парой методов, которые синтаксически оформлены для представления в вызывающем коде, как будто свойство является полем. Они существуют для ситуации, когда интуитивно удобнее вызывать метод с помощью синтаксиса поля, очевидным примером будет случай закрытого поля, которое должно быта инкапсулировано с помощью оболочки из открытых методов доступа. Предположим, что класс имеет такое поле length типа int. Тогда в C++ оно инкапсулируется с помощью методов GetLength() и SetLength(). Необходимо будет обращаться к нему извне класса:

// MyObject является экземпляром рассматриваемого класса

MyObject.SetLength(10);

int Length = MyObject.GetLength();

В C# можно реализовать эти методы, как аксессоры (методы доступа) get и set свойства Length. Тогда запишем

// MyObject является экземпляром рассматриваемого класса

MyObject.Length = 10;

int length = MyObject.Length;

Чтобы определись эти методы доступа, свойство будет определяться следующим образом:

class MyClass {

private int length;

public int Length {

get {

return length;

}

set {

Length = value;

}

Хотя методы доступа get и set реализованы здесь, чтобы просто возвращать или задавать поле length, в эти методы можно поместить любой другой требуемый код C# так же, как это обычно делается в методе. Например, добавить некоторую проверку данных в метод доступа set. Отметим, что метод доступа set возвращает void и получает дополнительный неявный параметр с именем value.

Можно опустить любой из методов доступе get или set из определения свойства, и в этом случае свойство осуществляет соответственно либо только запись, либо только чтение.

Операторы

Значение и синтаксис операторов в большинстве случаев те же в C#, что и в C++. Следующие операторы по умолчанию имеют в C# такое же значение и синтаксис как и в C++:

□ Бинарные арифметические операторы +, -, *, /, %

□ Соответствующие арифметические операторы присваивания +=, -=, *=, /=, %=

□ Унарные операторы ++ и -- (обе — префиксная и постфиксная формы)

□ Операторы сравнения !=, ==, <, <=, >=

□ Операторы сдвига >> и <<

□ Логические операторы &, |, &&, ||, ~, ^, !

□ Операторы присваивания, соответствующие логическим операторам: >>=, <<=, &=, |=, ^=

□ Тернарный (условный) оператор

Символы (), [], и , (запятая) также имеют в общих чертах такой же эффект в C#, как и в C++.

Необходимо быть осторожным со следующими операторами, так как они действуют в C# иначе, чем в C++:

□ Присваивание (=), new, this.

Оператор разрешения области видимости в C# представлен ., а не :: (:: не имеет смысла в C#). Также в C# не существуют операторы delete и delete[]. Они не нужны, так как сборщик мусора автоматически управляет очисткой памяти в куче. Однако C# предоставляет также три других оператора, которые не существуют в C++, а именно, is, as и typeof. Эти операторы связаны с получением информации о типе объекта или класса.

Оператор присваивания (=)

Для простых типов данных = просто копирует данные. Однако при определении своих собственных классов C++ считает в большой степени, что обязанность разработчика указать значение = для этих классов. По умолчанию в C++ = требует поверхностного почленного копирования всех переменных, классов или структур. Однако программисты перезагружают этот оператор для выполнения более сложных операций присваивания.

В C# правила, определяющие, что означает оператор присваивания, значительно проще. Вообще не разрешается перезагружать =, его значение неявно определено во всех ситуациях.

Ситуация в C# будет следующая:

□ Для простых типов данных = просто копирует значения, как в C++.

□ Для структур = делает поверхностное копирование структуры — прямую копию памяти данных в экземпляре структуры. Это аналогично поведению в C++.

□ Для классов = копирует ссылку, то есть адрес, а не объект. Это не соответствует поведению в C++.

Если требуется скопировать экземпляры классов, обычный способ в C# состоит в переопределении метода MemberwiseCopy(), который все классы в C# по умолчанию наследуют из класса System.Object — общего класса-предка, из которого неявно выводятся все классы C#.

this

Оператор this имеет то же самое значение, что и в C++, но это скорее ссылка, а не указатель. Например, в C++ можно записать:

this->m_MyField = 10;

В C# это будет выглядеть так:

this.MyField = 10;

this используется в C# таким же образом, как и в C++. Например, можно передавать его в качестве параметра в вызовах методов или использовать его, чтобы сделать явным доступ к полю-члену класса. В C# существует пара других ситуаций, которые синтаксически требуют использования this, о них будет упомянуто в разделе о классах.

new

Как сообщалось ранее, оператор new, интерпретируемый как конструктор, имеет другое значение в C#, поскольку он обеспечивает инициализацию объекта а не запрос динамического выделения памяти.

Классы и структуры

В C++ классы и структуры очень похожи. Формально единственное различие состоит в том, что члены структуры являются по умолчанию открытыми, в то время как члены класса являются по умолчанию закрытыми. На практике, однако, многие программисты предпочитают использовать структуры и классы различным образом, сохраняя использование структур для объектов данных, которые содержат только члены-переменные (другими словами, без функций членов или явных конструкторов).

C# отражает это традиционное различие использования. В C# класс — это совершенно другой тип объектов, по сравнению со структурой, поэтому нет необходимости тщательно рассматривать, будет ли лучше определить заданный объект как класс или как структуру. Наиболее важные различия между классами C# и структурами C# следующие:

□ Структуры не поддерживают наследование, кроме того факта, что они являются производными из System.ValueType. Невозможно наследовать от структуры и структура не может наследовать от другой структуры или класса.

□ Структуры являются типами данных значений. Классы всегда являются ссылочными типами данных.

□ Структуры позволяют организовать способ размещения полей в памяти и определяют эквивалент объединений C++.

□ Конструктор структуры по умолчанию (без параметров; всегда поставляется компилятором и не может быть заменен.

Поскольку классы и структуры сильно отличаются в C#, они в этом приложении рассматриваются по отдельности.

Классы

Классы в C# следуют в основном тем же самым принципам, что и в C++, однако существует разница в свойствах и синтаксисе. Мы рассмотрим отличия между классами C++ и классами C# в этом разделе.

Определение класса

Классы определяются в C# с помощью синтаксиса, который на первый взгляд выглядит как синтаксис C++:

class MyClass : MyBaseClass {

private string SomeField;

public int SomeMethod() {

return 2;

}

}

За этим первоначальным сходством скрываются многочисленные различия в деталях.

□ Не существует модификатора доступа по имени базового класса. Наследование всегда открытое.

□ Класс может быть выведен только из одного базового класса (хотя из любого числа интерфейсов). Если базовый класс явно не определен, то класс будет автоматически выводиться из System.Object, который предоставит ему всю функциональность System.Object, из которой чаще всего используется ToString().

□ Каждый член явно объявляется с модификатором доступа. Не существует эквивалента синтаксису C++, где один модификатор доступа может применяться к нескольким членам.

public: // нельзя использовать этот синтаксис в C#

int MyMethod();

int MyOtherMethod();

□ Методы не могут объявляться как inline. Это связано с тем, что C# компилируется в промежуточный язык (IL). Любые вставки кода происходят на второй стадии компиляции, когда JIT-компилятор выполняет преобразование из IL в собственный код машины. JIT-компилятор имеет доступ ко всей информации в IL при определении, какие методы могут подходить для вставки без необходимости каких-либо указаний в исходном коде разработчика.

□ Реализация методов всегда помещается вместе с определением. Невозможно написать реализацию вне класса, как позволяет C++.

□ В то время как в ANSI C++ единственными типами члена класса являются переменные, функции, конструкторы, деструкторы и перезагружаемые версии операторов, C# имеет в наличии также делегатов, события и свойства.

□ Модификаторы доступа public, private и protected обладают тем же самым значением, как и в C++, но существуют два дополнительных модификатора доступа:

□ internal ограничивает доступ к другому коду внутри той же сборки.

□ protected internal ограничивает доступ к производным классам, которые находятся внутри той же сборки.

□ Инициализация переменных разрешается в C# в определении класса.

□ В C++ ставится точка с запятой после закрывающейся фигурной скобки в конце определения класса. Это не требуется в C#.

Инициализация полей членов

Синтаксис, используемый для инициализации полей членов в C#, очень отличается от синтаксиса C++, хотя конечный результат одинаковый.

Члены экземпляра

В C++ поля членов экземпляра обычно инициализируются в списке инициализации конструктора:

MyClass::MyClass() : m_MyField(6) {

// и т.д.

В C# этот синтаксис недопустим. Можно помещать в инициализатор конструктора (который является эквивалентом C# списка инициализации конструктора в C++) другой конструктор. Вместо этого инициализированное значение помечается с помощью определения члена в определении класса:

class MyClass {

private int MyField = 6;

Отметим, что в C++ это будет ошибкой, так как C++ использует примерно такой же синтаксис для определения чисто виртуальных функций. В C# такое действие считается нормой, так как C# не применяет синтаксис =0 для этой цели (он использует вместо этого ключевое слово abstract).

Статические поля

В C++ статические поля инициализируются с помощью отдельного определения вне класса:

int MyClass:MyStaticField = 6;

На самом деле в C++, даже если не требуется инициализировать статическое поле, необходимо включить эту инструкцию, чтобы избежать ошибки компоновки. В противоположность этому C# не ожидает подобной инструкции, так как в C# переменные объявляются только в одном месте:

Class MyClass {

private static int MyStaticField = 6;

Конструкторы

Синтаксис объявления конструкторов в C# такой же, как синтаксис для встраиваемых конструкторов, заданных в определении класса в C++.

class MyClass {

publiс MyClass() {

// код конструктора

}

Как и в C++, можно определить столько конструкторов, сколько потребуется, при условии, что они получают различное число или типы параметров. (Отметим, что, как и в методах, параметры по умолчанию не допускаются, необходимо моделировать это с помощью нескольких перезагружаемых версий.)

Для производных классов иерархии конструкторы действуют в C# по сути таким же образом, как и в C++. По умолчанию конструктор на вершине иерархии (это всегда System.Object) выполняется первым, за ним следуют конструкторы в порядке, определяемом деревом иерархии.

Статические конструкторы

C# допускает также концепцию статического конструктора, который выполняется только один раз и может использоваться для инициализации статических переменных. Концепция не имеет прямого эквивалента в C++.

class MyClass {

static MyClass() {

// код статического конструкторе

}

Статические конструкторы очень полезны тем, что позволяют инициализировать статические поля с помощью значений, которые определяются во время выполнения (например, они могут задаваться значениями которые считываются из базы данных). Такой результат возможен в C++, но требует определенной работы, и решение обычно выглядит беспорядочным. Наиболее общий подход должен иметь функцию, которая обращается к статическим переменным членам, и функция будет реализована таким образом, что она задает значение переменной при первом вызове.

Отметим, что статический конструктор не имеет спецификатора доступа, он не объявляется как открытый, закрытый или как-нибудь еще. Спецификатор доступа не будет иметь смысла, так как статический конструктор вызывается только средой выполнения .NET, когда загружается определение класса. Он не может вызываться никаким другим кодом C#.

C# не задает точно, когда будет выполнен статический конструктор, за исключением только того, что это произойдет после инициализации всех статических полей, но перед тем, как будет создан какой-либо объект класса, или там, где статические методы класса реально используются.

Конструкторы по умолчанию

Как и в C++, классы C# обычно имеют конструктор по умолчанию без параметров, который просто вызывает конструктор без параметров непосредственного базового класса, а затем инициализирует все поля их параметрами по умолчанию. Так же как в C++, компилятор будет создавать этот конструктор по умолчанию, только если в коде явно не предоставлен никакой другой конструктор. Если какие-либо конструкторы присутствуют в определении класса, то в этом случае будут доступны только эти конструкторы, независимо от того, есть или нет среди них конструктор без параметров.

Как и в C++ можно обойтись без создания экземпляров класса, объявляя закрытый конструктор единственным.

class MyClass {

private MyClass() {

}

Это также не позволяет создавать экземпляры любых производных классов. Однако, если класс или методы в нем объявлены абстрактными, то нельзя создать экземпляр этого класса, причем не обязательно производного класса.

Списки инициализации конструктора

Конструкторы C# могут иметь элементы, которые выглядят как списки инициализации конструктора C++. Однако в C# такой список содержит только максимум один член и называется инициализатором конструктора. Элемент в инициализаторе должен быть либо конструктором непосредственного базового класса, либо другим конструктором того же класса. Синтаксис этих двух вариантов использует ключевые слова base и this соответственно:

class MyClass : MyBaseClass {

MyClass(int X)

: base(X) // выполняет конструктор MyBaseClass с одним параметром

{

// здесь другая инициализация

}

MyClass()

: this(10) // выполняет конструктор MyClass с одним параметром,

// передавая в него значение 10

{

// здесь другая инициализация

}

Если явно не задан никакой список инициализации конструктора, то компилятор будет неявно использовать список из элемента base(). Другими словами, инициализатор по умолчанию вызывает конструктор по умолчанию базового класса. Это поведение совпадает с C++.

В отличие от C++ нельзя поместить переменные члены в список инициализации конструктора. Однако это только вопрос синтаксиса, так как эквивалент C# должен отметить свои начальные значения в определении класса.

Более серьезным различием является тот факт, что можно поместить только один иной конструктор в список. Это влияет на способ разработки конструкторов, хотя несомненно полезно, так как заставляет использовать хорошо определенную и эффективную парадигму организации конструкторов. Эта парадигма указана в приведенном выше коде. Все конструкторы следуют единому порядку, в котором выполняются различные конструкторы.

Деструкторы

C# реализует отличную от C++ модель программирования деструкторов. Это связано с тем, что механизм сборки мусора в C# предполагает следующее:

□ Существует меньшая необходимость в деструкторах, так как динамически распределенная память будет удаляться автоматически.

□ Так как невозможно предсказать, когда сборщик мусора реально разрушит заданный объект, то если для класса предоставляется деструктор, невозможно предсказать в точности, когда этот деструктор будет выполнен.

Поскольку память очищается в C# "за сценой", то оказывается, что только небольшое число классов действительно нуждается в деструкторах. Для тех, кому это нужно (это классы, которые поддерживают внешние неуправляемые ресурсы, такие как соединения с файлами или с базами данных), C# имеет двухэтапный механизм разрушения:

1. Класс должен выводиться из интерфейса IDisposable и реализовывать метод Dispose(). Этот метод предназначен для явного вызова с помощью кода клиента для указания, что он закончил работать с объектом и требуется очистить ресурсы. (Интерфейсы будет рассмотрены позже в этом приложении.)

2. Класс должен отдельно реализовать деструктор, который рассматривается как запасной механизм, на случай, если клиент не вызывает Dispose().

Обычная реализация Dispose() выглядит следующим образом:

public void Dispose() {

// очистка ресурсов

System.GC.SuppressFinalize(this);

}

System.GC является базовым классом, представляющим сборщика мусора. SuppressFinalize() является методом, который информирует сборщика мусора, что нет необходимости вызывать деструктор для разрушаемого объекта. Вызов SuppressFinalize() важен, так как имеется снижение производительности, если в объекте есть деструктор, который нужно вызывать в то время, когда сборщик мусора выполняет свою работу. Следствием этого является то, что реальное освобождение управляемой памяти, занимаемой этим объектом, будет существенно задерживаться.

Синтаксис деструктора по сути такой же в C#, как и в C++. Отметим, что в C# не требуется объявлять деструктор виртуальным, компилятор будет это подразумевать. Не требуется также предоставлять модификатор доступа:

Class MyClass {

~MyClass() {

// очистка ресурсов

}

}

Хотя метод Dispose() обычно явно вызывается клиентами, C# допускает альтернативный синтаксис, который гарантирует, что компилятор примет меры, чтобы он был вызван. Если переменная объявлена внутри блока using(), то ее область действия совпадает с блоком using и ее метод Dispose() будет вызываться при выходе из блока:

using (MyClass MyObject = new MyClass()) {

// код

} // MyObject.Dispose() будет неявно вызван при выходе из этого блока

Отметим, что приведенный выше код будет компилироваться успешно только если MyClass выводится из интерфейса IDisposable и реализует метод Dispose(). Если нежелательно использовать синтаксис using, то можно опустить один или оба из двух шагов в последовательности деструктора (реализация Dispose() и реализация деструктора), но обычно реализуются оба шага. Можно также реализовать Dispose(), без привлечения интерфейса IDisposable, но если это делается, то снова невозможно использовать синтаксис using, чтобы для экземпляров этого класса Dispose() вызывался автоматически.

Наследование

Наследование работает в основном таким же образом в C#, как и в C++, с тем исключением, что множественная реализация наследования не поддерживается. Компания Microsoft считает, что множественное наследование ведет к коду, который хуже структурирован и который труднее сопровождать, и поэтому решила исключить это свойство из C#.

Class MyClass : MyBaseClass {

// и т.д.

В C++ указатель на класс может дополнительно указывать на экземпляр производного класса. (Виртуальные функции в конце концов зависят от этого факта.) В C# классы доступны через ссылки, но правило остается тем же. Ссылка на класс может ссылаться на экземпляры этого класса или на экземпляры любого производного класса.

MyBaseClass Mine;

Mine = new MyClass(); // все нормально, если MyClass будет производным

// от MyBaseClass

Если желательно, чтобы ссылка ссылалась на произвольный объект (эквивалент void* в C++), можно определить ее как object в C#, так как C# отображает object в класс System.Object, из которого выводятся все другие классы.

object Mine2 = new MyClass();

Виртуальные и невиртуальные функции

Виртуальные функции поддерживаются в C# таким же образом, как и в C++. Однако в C# существуют некоторые синтаксические отличия, которые созданы, чтобы исключить возможную неоднозначность в C++. Это означает, что некоторые типы ошибок, которые появляются в C++ только во время выполнения, будут идентифицированы в C# во время компиляции.

Отметим также, что в C# классы всегда доступны по ссылке (что эквивалентно доступу через указатель в C++).

Если в C++ требуется, чтобы функция была виртуальной необходимо просто определить ключевое слово virtual в базовом и производном классах. В противоположность этому в C# необходимо объявить функцию как virtual в базовом классе и как override в версиях производных классов.

class MyBaseClass {

public virtual void DoSomething(int X) {

// и т.д.

}

// и т.д.

}

class MyClass : MyBaseClass {

public override void DoSomething(int X) {

// и т.д.

}

// и т.д.

}

Важный момент этого синтаксиса состоит в том, что он дает понять компилятору, как интерпретировать функцию, и значит, исключается риск таких ошибок, где, например, вводится слегка неправильная сигнатура метода в переопределяемой версии, и поэтому определяется новая функция вместо переопределения существующей. Компилятор будет отмечать ошибку, если функция помечена как override, и компилятор не сможет идентифицировать ее версию в каком-либо базовом классе.

Если функция не является виртуальной, то можно все равно определить версии этого метода в производном классе, в этом случае говорят, что версия производного класса скрывает версию базового класса, а вызываемый метод зависит только от типа ссылки, используемой для доступа к классу, так же как это зависит от типа указателя, используемого для доступа к классу в C++.

В случае, если в C# версия функции в производном классе скрывает соответствующую функцию в базовом классе, можно явно указать это с помощью ключевого слова new.

class MyBaseClass {

public void DoSomething(int X) {

// и т.д.

}

// и т.д.

}

class MyClass : MyBaseClass {

public new void DoSomething(int X) {

// и т.д.

} и т.д.

}

Если не пометить новую версию класса явно как new, то код по-прежнему будет компилироваться, но компилятор будет выдавать предупреждение. Предупреждение служит для защиты от любых трудноуловимых ошибок, возникающих во время выполнения. Например, когда написана новая версия базового класса, в которой добавлен метод, имеющий, оказывается, такое же имя, как и существующий метод в производном классе.

В C# можно объявить абстрактную функцию, также как это делается в C++ (в C++ она называются еще чисто виртуальной функцией), но в C# синтаксис будет отличаться: вместо использования =0 в конце определения применяется ключевое слово abstract.

C++:

public:

virtual void DoSomething(int X) = 0;

C#:

public abstract void Dosomething(int X);

Как и в C++, можно создать экземпляр класса, только если он сам не содержит абстрактных методов и предоставляет реализации всех абстрактных методов, которые были определены в любом из его базовых классов.

Структуры

Синтаксис определения структур в C# соответствует синтаксису определения классов.

struct MyStruct {

private SomeField;

public int SomeMethod() {

return 2;

}

}

Наследование и связанные концепции, виртуальные и абстрактные функции не допускаются. В остальном базовый синтаксис идентичен синтаксису классов, за исключением того, что ключевое слово struct заменяет в определении class.

Существуют, однако, различия между структурами и массами, когда дело доходит до создания. В частности, структуры всегда имеют конструктор по умолчанию, который обнуляет все поля, и этот конструктор по-прежнему присутствует, даже если определены другие собственные конструкторы. Также невозможно явно определить конструктор без параметров для замены конструктора по умолчанию. Можно определить только конструкторы с параметрами. В этом отношении структуры в C# отличаются от своих аналогов в C++.

В отличие от классов в C#, структуры являются типом данных значений. Это означает, что такая инструкция как:

MyStruct Mine;

реально создает экземпляр MyStruct в стеке. Однако в C# этот экземпляр не инициализируется, если конструктор не вызван явно:

MyStruct Mine = new MyStruct();

Если все поля-члены из MyStruct являются открытыми, можно альтернативно инициализировать структуру, преобразуя каждое поле-член по отдельности.

Константы

Ключевое слово const в C++ имеет достаточно большой диапазон использования. Например, можно объявить переменные как const, тем самым указывая, что их значения обычно задаются во время компиляции и не могут изменяться никакой инструкцией присваивания во время выполнения (хотя существует небольшой элемент гибкости, так как значение члена переменной const может быть задано в списке инициализации конструктора, а это предполагает в данном случае, что значение может вычисляться во время выполнения). Можно также применять const к указателям и ссылкам, чтобы запретить их использование для изменения данных, на которые они указывают, и можно также использовать ключевое слово const для модификации определений параметров, передаваемых в функции. В этом случае const показывает, что переменная, которая была передана по ссылке или через указатель, не должна изменяться функцией. Как упоминалось ранее, сами члены функции тоже могут быть объявлены как const, чтобы подчеркнуть, что они не изменяют содержащий их экземпляр класса.

C# позволяет также использовать ключевое слово const для указания, что переменная не может изменяться. Во многих отношениях, однако, применение const более ограничено в C#, чем в C++. В C# единственное использование const состоит в фиксировании значения переменной (или элемента, на который указывает ссылка) во время компиляции. Оно не может применяться к методам или параметрам. С другой стороны, C# имеет преимущества перед C++ в том смысле, что синтаксис в C# допускает немного больше гибкости при инициализации полей const во время выполнения.

Синтаксис объявления констант различается в C# и C++, поэтому мы рассмотрим его более подробно. Синтаксис C# использует два ключевых слова — const и readonly. Ключевое слово const предполагает, что значение задается во время компиляции, в то время как readonly предполагает, что оно задается однажды во время выполнения в конструкторе.

Так как все в C# должно быть членом класса или структуры, не существует, конечно, прямого эквивалента в C# для глобальных констант C++. Эту функциональность можно получить с помощью перечислений или статических полей-членов класса.

Константы, ассоциированные с классом (статические константы)

Обычный способ определения статической константы в C++ состоит в записи члена класса как static const. C# делает это похожим образом, но с помощью более простого синтаксиса:

Синтаксис C++:

int CMyClass::MyConstant = 2;

class CMyClass {

public:

static const int MyConstant;

Синтаксис C#:

class MyClass {

public const int MyConstant = 2;

Отметим, что в C# константа не определяется явно как static, если это сделать, то возникнет ошибка компиляции. Она является, конечно, неявно статической, так как не существует возможности задать значение константы более одного раза, и, следовательно, она всегда должна быть доступна как статическое поле.

int SomeVariable = MyClass.MyConstant;

Ситуация становится интереснее, если статическую константу инициализировать некоторым значением, которое вычисляется во время выполнения. C++ не имеет средств, чтобы это сделать. Для достижения такого результата потребуется найти некоторые возможности инициализировать переменную при первом обращении к ней, что означает, что ее прежде всего невозможно объявить как const. В случае C# статические константы инициализируются во время выполнения. Поле определяется как readonly и инициализируется в статическом конструкторе.

class MyClass {

public static readonly int MyConstant;

static MyClass() {

// определяет и присваивает начальное значение MyConstant

}

Константы экземпляра

Константы, которые ассоциированы с экземплярами класса, всегда инициализируются значениями, вычисленными во время выполнения. (Если их значения были вычислены во время компиляции, то, по определению, это делает их статическими.)

В C++ такие константы должны быть инициализированы в списке инициализации конструктора класса. Это в некоторой степени ограничивает гибкость при вычислении значений этих констант, так как начальное значение должно быть таким, чтобы его можно было записать как выражение в списке инициализации конструктора.

class CMyClass {

public:

const int MyConstInst;

CMyClass() : MyConstInst(45); {

В C# принцип похож, но константа объявляется как readonly, а не как const. Таким образом, ее значение задается в теле конструктора, придавая гибкость процессу, так как можно использовать любые инструкции C# при вычислении начального значения. (Вспомните, что в C# невозможно задать значения переменных в инициализаторе конструктора, только вызвать другой конструктор.)

class MyClass {

public readonly int MyConstInst;

MyClass() {

// определяет и инициализирует здесь MyConstInst

Если поле в C# объявлено как readonly, то ему можно присвоить значение только в конструкторе.

Перезагрузка операторов

Перезагрузка операторов происходит в C# и в C++ аналогично, но существуют небольшие различия. Например, C++ допускает перезагрузку большинства своих операторов. C# имеет больше ограничений. Для многих составных операторов C# автоматически определяет значение оператора из значений составляющих операторов, т.е. там, где C++ допускает прямую перезагрузку. Например, в C++ перезагружается + и отдельно +=. В C# можно перезагрузить только +. Компилятор всегда будет использовать перезагруженную версию +, чтобы автоматически определить значение += для этого класса или структуры.

Следующие операторы могут перезагружаться в C# также, так и в C++:

□ Бинарные арифметические операторы +, -, *, /, %

□ Унарные операторы ++ и -- (только префиксная форма)

□ Операторы сравнения !=, ==, <, <=, >=

□ Побитовые операторы &, |, ~, ^, !

□ Булевы значения true и false

Следующие операторы, перезагружаемые в C++, нельзя перезагружать в C#.

□ Арифметические операторы присваивания *=, /=, +=, -=, %=. (Они определяются компилятором из соответствующих арифметических операторов и оператора присваивания, который не может перезагружаться.) Постфиксные операторы увеличения на единицу. Они определяются компилятором из перезагруженных версий соответствующих префиксных операторов. (Реализуются с помощью вызова соответствующей перезагруженной версии префиксного оператора, но возвращают первоначальное значение операнда вместо нового значения.)

□ Операторы побитового присваивания &=, | =, ^=, >>= и <<=.

□ Булевы операторы &&, ||. (Они определяются компилятором из соответствующих побитовых операторов.)

□ Оператор присваивания =. Значение этого оператора в C# фиксировано.

Существует также ограничение в том, что операторы сравнения должны перезагружаться парами, другими словами, при перезагрузке == необходимо перезагрузить также != и наоборот. Аналогично, если перезагружается один из операторов < и <=, то необходимо перезагрузить оба оператора и так же для > и >=. Причина этого состоит в необходимости обеспечения согласованной поддержки для любых типов данных базы данных, которые могут иметь значение null и для которых поэтому, например, == не обязательно имеет результат, противоположный !=.

После определения того, что оператор, который требуется перезагрузить, является таким, который можно перезагрузить в C#, синтаксис для реального определения перезагруженной версии значительно проще, чем соответствующий синтаксис в C++. Единственный момент, о котором необходимо помнить при перегрузке операторов C#, состоит в том, что они всегда должны объявляться как статические члены класса. Это противоположно ситуации в C++, где можно определить свои операторы либо как статические члены класса, как член экземпляра класса (но беря на один параметр меньше), либо как функцию, которая не является членом класса вообще.

Причина того, что определение перезагруженных версий операторов настолько проще в C#, не имеет на самом деле ничего общего с самими перезагруженными версиями операторов. Это связано со способом, которым осуществляется управление памятью в C#. Определение перезагруженных версий операторов в C++ является областью, которая заполнена ловушками, Рассмотрим, например, попытку перезагрузить оператор сложения для класса в C++. (Предполагается для этого, что CMyClass имеет член x и сложение экземпляров означает сложение членов x.). Код может выглядеть следующим образом (предполагается, что перезагруженная версия является явной вставкой кода):

static CMyClass operator+(const CMyClass &lhs, const CMyClass &rhs) {

CMyClass Result;

Result.x = lhs.x + rhs.x;

return Result;

}

Отметим, что оба параметра объявлены как const и передаются по ссылке, чтобы обеспечить оптимальную производительность. Это само по себе не слишком плохо. Однако теперь для возвращения результата, необходимо создать временный экземпляр CMyClass внутри перезагруженной версии оператора. Конечная инструкция return Result выглядит безопасной, но она будет компилироваться только в том случае, если доступен оператор присваивания для копирования Result из функции.

Это само по себе является нетривиальной задачей, так как если ссылки используются неправильно при определении, то очень легко случайно определить ссылку, которая рекурсивно вызывает себя, пока не будет получено переполнение стека. Перезагрузка операторов в C++ является задачей для опытных программистов. Нетрудно видеть, почему компания Microsoft решила сделать некоторые операторы неперезагружаемыми в C#.

В C# практика будет другой. Здесь нет необходимости явно передавать по ссылке, так как классы C# являются ссылочными переменными в любом случае (а для структур передача по ссылке снижает производительность). И возвращение значения является легкой задачей. Будет ли это класс или структура, надо просто вернуть значение временного результата, а компилятор C# гарантирует, что в результате будут  скопированы либо поля-члены (для типов данных значений), либо адреса (для ссылочных типов). Единственный недостаток заключается в том, что нельзя использовать ключевое слово const, чтобы получить дополнительную проверку компилятора, которая определяет, изменяет или нет перезагруженная версия оператора параметры класса. Также C# не предоставляет улучшения производительности подставляемых функций, как происходит в C++.

static MyClass operator+(MyClass lhs, CMyClass rhs) {

MyClassResult = new MyClass();

Result.x = lhs.x + rhs.x;

return Result;

}

Индексаторы

C# cтрого не разрешает перезагружать []. Однако он позволяет определить так называемые индексаторы (indexer) класса, что обеспечивает такой же результат.

Синтаксис определении индексатора очень похож на синтаксис свойства. Предположим, что необходимо использовать экземпляры MyClass как массив, где каждый элемент индексируется с помощью int и возвращает long. Тогда можно сделать следующую запись:

class MyClass {

public long this[int x] {

get {

// код для получения элемента

}

set {

// код для задания элемента, например X = value;

}

}

// и т.д.

Код внутри блока get выполняется всякий раз, когда Mine[x] стоит с правой стороны выражения (при условии, что выражение Mine является экземпляром MyClass и x будет int), в то время как блок set выполняется только тогда, когда Mine[x] указывается с левой стороны выражения. Блок set ничего не может вернуть и использует ключевое слово value для указания величины, которая появится с правой стороны выражения. Блок get должен вернуть тот же тип данных, который имеет индексатор.

Можно перезагружать индексаторы, чтобы использовать любой тип данных в квадратных скобках или любое число аргументов, тем самым создавая эффект многомерного массива.

Определенные пользователем преобразования типов данных

Так же как для индексаторов и [], C# формально не рассматривает () как оператор, который может перезагружаться, однако C# допускает определяемые пользователем преобразования типов данных, которые имеют тот же результат. Например, предположим, что имеются два класса (или структуры) с именами MySource и MyDest и необходимо определить преобразование типа из MySource в MyDest. Синтаксис этого выглядит следующим образом:

public static implicite operator MyDest(MySource Source) {

// код для выполнения преобразования. Должен возвращать экземпляр MyDest

}

Преобразование типа данных определяется как статический член класса MyDest или класса MySource. Оно должно также объявляться любо как implicit, либо как explicit. Если преобразование объявлено как implicit, то оно используется неявно:

MySource Source = new MySource();

MyDest Dest = MySource;

Если преобразование объявлено как explicit, то оно может использоваться только явным образом:

MySource Source = new MySource();

MyDest Dest = (MyDest)MySource;

Необходимо определять неявные преобразования типов данных в том случае, когда они всегда работают, а явные преобразования типов только тогда, когда может произойти отказ в связи с потерей данных или порождением исключения.

Так же как и в C++, если компилятор C# встречается с запросом преобразования между типами данных, для которых не существует прямого преобразования типов, он будет стараться найти "лучший" способ, используя доступные методы преобразования типов. Существуют те же вопросы, что и в C++, в отношении интуитивной ясности преобразований типов данных, а также в том, что различные пути получения преобразования не создают несовместимых результатов.

C# не позволяет определить преобразования типов данных между классами, которые являются производными друг друга. Такие преобразования уже доступны — неявно из производного класса в базовый класс и явно из базового класса в производный.

Отметим, что если попробовать выполнить преобразование ссылки базового класса в ссылку производного класса, и при этом рассматриваемый объект не является экземпляром производного класса (или какого-нибудь производного из него), то будет порождаться (генерироваться) исключение. В C++ нетрудно преобразовать указатель на объект в "неправильный" класс объектов. Это просто невозможно в C# с помощью ссылок. По этой причине преобразование типов в C# считается более безопасным, чем в C++.

// пусть MyDerivedClass получен из MyBaseClass

MyBaseClass MyBase = new MyBaseClass();

MyDerivedClass MyDerived = (MyDerivedClass) MyBase; // это приведет

// к порождению исключения

Если нежелательно преобразовывать что-то в производный класс, но нежелательно также, чтобы генерировалось исключение, можно применить ключевое слово as. При использовании as, если преобразование отказывает, будет возвращаться null.

// пусть MyDerivedClass получен из MyBaseClass

MyBaseClass MyBase = new MyBaseClass();

MyDerivedClass MyDerived as (MyDerivedClass)MyBase; // это

// возвратит null

Массивы

Массивы являются одной из областей, в которой внешнее сходство в синтаксисе между C++ и C# скрывает то, что реально происходящее "за сценой" существенно различается в этих двух языках. В C++ массив является по сути множеством переменных, упакованных вместе в памяти и доступных через указатель. В C#, с другой стороны, массив является экземпляром базового класса System.Array, и поэтому выступает полноценным объектом, хранящимся в куче под управлением сборщика мусора. Для доступа к методам этого класса C# использует синтаксис типа C++ способом, который создает иллюзию доступа к массиву. Недостаток этого подхода состоит в том, что накладные расходы для массивов больше, чем в C++, но преимуществом является то, что массивы C# более гибкие и при этом проще кодируются. В качестве примера: все массивы C# имеют свойство Length, которое задает число элементов массива, не требуя тем самым хранить его отдельно. К тому же массивы C# значительно безопаснее в использовании — так, проверка границ индекса выполняется автоматически.

В C# возможно сделать простой массив без накладных расходов класса System.Array, но для этого понадобится использовать указатели и ненадежные блоки кода.

Одномерные массивы

Для одномерных массивов (терминология C#: массивы ранга 1) синтаксис доступа в обоих языках идентичен — с квадратными скобками, используемыми для индикации элементов массива. Массивы начинаются с нулевого индекса в обоих языках.

Например, чтобы умножить каждый элемент массива float на 2:

// массив, объявлен как массив float

// этот код работает в C++ и C# без каких-либо изменений

for (int i = 0; i < 10; i++) Array[i] = 2.0f;

Однако, как упоминалось ранее, массивы C# поддерживают свойство Length, которое используется для определения числа элементов в массиве.

// массив, объявлен как массив float

// этот код компилируется только в C#

for (int i = 0; i < Array.Length; i++) Array[i] *= 2.0f;

В C# можно также использовать инструкцию foreach для доступа к элементам массива, что рассматривалось ранее.

Синтаксис объявления массивов в C# слегка отличается, так как массивы C# всегда объявляются как ссылочные объекты.

double [] Array; // простое объявление ссылки без реального

// создания экземпляра массива

Array = new double[10]; // реально создается экземпляр объекта

// System.Array и задается размер 10.

Или, объединяя эти инструкции, запишем:

double [] array = new double[10];

Отметим, что размер массива задается только вместе с созданием его экземпляра. Объявление ссылки использует просто квадратные скобки для указания, что размерность (ранг) массива будет единица. В C# ранг считается частью типа массива, в отличие от числа элементов.

Ближайший эквивалент в C++ приведенного выше определения будет выглядеть так:

double *pArray = new double[10];

Эта инструкция C++ действительно дает достаточно близкую аналогию, так как обе версии C++ и C# размещаются в куче. Отметим, что версия C++ является просто областью памяти, которая содержит 10 double, в то время как версия C# создает экземпляр полноценного объекта. Более простая стековая версия C++:

doublе pArray[10];

не имеет аналога в C#, который использует реальный массив C#, хотя инструкция C# stackalloc может создать эквивалент этой инструкции с помощью указателей. Об этом мы будем говорить позже в разделе, в котором рассматривается ненадежный код.

Массивы в C# можно явно инициализировать при создании экземпляра:

double [] Array = new double[10] {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 9.0, 10.0};

Существует также более короткая форма:

double [] Array = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 9.0, 10.0};

Если массив не инициализирован явно, то будет автоматически вызываться конструктор по умолчанию для каждого из его элементов. (Элементы массива формально рассматриваются как поля-члены класса.) Это поведение отличается от C++, где не допускается никакой автоматической инициализации массивов, размещенных с помощью оператора new в куче (хотя C++ допускает это для массивов на основе стека).

Многомерные массивы

C# существенно отклонился от C++ в вопросе многомерных массивов, так как C# поддерживает как прямоугольные, так и неровные массивы.

Прямоугольный массив является правильной сеткой чисел. В C# это указывается синтаксисом, где запятая разделяет число элементов в каждой размерности. Например, двухмерный прямоугольный массив, можно определить следующим образом:

int [,] MyArray2d;

MyArray2d = new int[2, 3] {{1, 0}, {3, 6}, {9, 12}};

Синтаксис здесь является интуитивно понятным расширением синтаксиса одномерных массивов. Список инициализации в таком коде может отсутствовать. Например:

int [,,] MyArray3d = new int [2, 3, 2];

Это приведет к вызову конструктора по умолчанию для каждого элемента и к инициализации каждого int нулем. В этом частном примере проиллюстрировано создание трехмерного массива. Общее число элементов в массиве равно 2×3×2 = 12. Характеристика прямоугольных массивов состоит в том, что все строки имеют одинаковое число элементов.

Элементы прямоугольного массива доступны с помощью следующего синтаксиса.

int X = MyArray3d[1, 2, 0] + MyArray2d[0, 1];

Прямоугольный массив C# не имеет прямых аналогов в C++. Однако неровные массивы в C# соответствуют достаточно точно многомерным массивам C++. Например, если объявить в C++ массив следующим образом:

int MyCppArray[3][5];

то реально объявляется не массив 3×5, а массив массивов — массив размера 3, каждый элемент которого является массивом размера 5. Это будет, возможно, понятнее, если сделать то же самое динамически. Запишем:

int pMyCppArray = new int[3];

for (int i=0; i<3; i++) pMyCppArray[i] = new int[5];

Из этого кода должно быть видно, что теперь не существует причины, чтобы каждая строка содержала одинаковое число элементов (хотя это вполне может быть, как в данном примере). В качестве примера неровного массива в C++, который имеет различное число элементов в каждой строке, можно написать:

int pMyCppArray = new int[3];

for (int i=0; i<3; i++) pMyCppArray[i] = new int[2*i + 2];

Соответствующие строки этого массива имеют размерности 2, 4 и 6. C# делает те же самые вещи почти таким же образом, хотя в случае C# синтаксис указывает число размерностей более явно:

int[][] MyJaggedArray = new int[3][];

for (int i = 0; i < 3, i++) MyJaggedArray[i] = new int[2*i + 2];

Доступ к членам неровного массива следует точно тому же синтаксису, что и в C++.

int X = MyJaggedArray[1][3];

Здесь показан неровный массив ранга 2. Однако так же, как и в C++, можно определить неровный массив с любым рангом, необходимо просто добавить прямоугольные скобки в его определение.

Проверка границ

Одной из областей, где объектная сущность массивов C# становится явной, является проверка границ. Если обратиться к элементу массива C#, указывая индекс, который не находится в границах массива, то это будет обнаружено во время выполнения и породит исключение IndexOutOfBoundsException. В C++ этого не происходит, в результате появляются трудноуловимые ошибки. C# выполняет дополнительную проверку ошибок за счет производительности. Хотя можно было бы ожидать, что это создаст потерю производительности, на самом деле здесь содержится преимущество, так как среда выполнения .NET способна контролировать код, чтобы гарантировать, что он является безопасным в том смысле, что не будет пытаться обратиться к памяти, которая не выделена для его переменных. Это обеспечивает выигрыш производительности, так как различные приложения могут, например, выполняться в одном процессе, и все равно есть уверенность, что эти приложения будут изолированы друг от друга. Имеется также выигрыш и в безопасности, так как возможно более точное предсказание, что данная программа будет или не будет пытаться делать.

С другой стороны, теперь нет ничего необычного в том, что программисты C++ используют какой-либо из различных классов оболочек массивов в стандартной библиотеке или в MFC, в основном для линейных массивов, чтобы получить тот же контроль границ и различные другие свойства, хотя в этом случае — без выигрыша в безопасности и производительности, связанных с возможностью анализа программы до ее выполнения.

Изменение размера массивов

Массивы C# являются динамическими, то есть можно определить число элементов в каждой размерности во время компиляции (также, как в динамически выделяемых массивах в C++). Однако невозможно изменить их размер после того, как были созданы их экземпляры. Если требуется это сделать, необходимо рассмотреть другие связанные с этим классы в пространстве имен System.Collections в библиотеке базовых классов, таких как System.Collections.ArrayList. Однако в этом отношении C# не отличается от C++. Обычные массивы C++ не допускают изменение размера, но существует ряд классов стандартной библиотеки, которые предоставляют это свойство.

Перечисления

В C# можно определить перечисление с помощью синтаксиса, аналогичного синтаксису C++.

// допустимо в C++ или C#

enum TypeOfBuilding {Shop, House, OfficeBlock, School};

Отметим, однако, что заключительная точка с запятой в C# не обязательна, так как определение перечисления в C# является фактически определением структуры, а определения структур не требуют заключительной точки с запятой.

// допустимо только в C#

enum TypeOfBuilding {Shop, House, OfficeBlock, School}

Однако в C# перечисление должно быть именованным, в то время как в C++ задание имени для перечисления является необязательным. Также как в C++, элементы перечисления в C# нумеруются от нуля в сторону увеличения, если только специально не определено, что элемент должен иметь определенное значение.

enum TypeOfBuilding {Shop, House=5, OfficeBlock, School = 10}

// Shop будет иметь значение 0, OfficeBlock будет иметь значение 6

Способ, с помощью которого происходит доступ к значениям элементов, отличается в C#, так как в C# необходимо определять имя перечисления.

Синтаксис C++:

TypeOfBuilding MyHouse = House;

Синтаксис C#:

TypeOfBuilding MyHouse = TypeOfBuilding.House;

Можно рассматривать это как недостаток, так как синтаксис очень велик, не это в действительности отражает тот факт, что перечисления являются в C# значительно более мощными. В C# каждое перечисление является полноценной структурой производной из System.Enum) и поэтому имеет некоторые методы. В частности, для любого перечисленного значения можно сделать следующее:

TypeOfBuilding MyHouse = TypeOfBuilding.House;

string Result = MyHouse.ToString(); // Result будет содержать "House"

Это почти невозможно сделать в C++.

В C# это делается и другим способом, с помощью статического метода Parse() класса System.Enum, хотя синтаксис будет чуть более запутанным

TypeOfВuilding MyHouse = (TypeOfBuilding)Enum.Parse(typeof(TypeOfBuilding), "House", true);

Enum.Parse() возвращает объектную ссылку и должен быть явно преобразован (распакован) обратно в соответствующий тип enum. Первым параметром в Parse() является объект System.Тyре, который описывает, какое перечисление должна представлять строка. Второй параметр является строкой, а третий параметр указывает, должен ли игнорироваться регистр символов. Вторая перегружаемая версия опускает третий параметр и не игнорирует регистр символов.

C# позволяет также выбрать описанный ниже тип данных, используемый для хранения enum:

enum TypeOfBuiding : short {Shop, House, OfficeBlock, School};

Если тип не указан, компилятор будет предполагать по умолчанию int.

Исключения

Исключения используются в C# таким же образом, как и в C++, кроме двух следующих различий:

□ C# определяет блок finally, который содержит код, всегда выполняющийся в конце блока try независимо от того, порождалось ли какое-либо исключение. Отсутствие этого свойства C++ явилось причиной недовольства среди разработчиков C++. Блок finally выполняется, как только управление покидает блок catch или try, и содержит обычно код очистки выделенных в блоке try ресурсов.

□ В C++ класс, порожденный в исключении, может быть любым классом. C#, однако, требует, чтобы исключение было классом, производным от System.Exception.

Правила выполнения программы в блоках try и catch идентичны в C++ и C#. Используемый синтаксис также одинаков, за исключением одного различия: в C# блок catch, который не определяет переменную для получения объекта исключения, обозначается самой инструкцией catch. Синтаксис C++:

catch (...) {

Синтаксис C#.

catch {

В C# этот вид инструкции catch может быть полезен для перехвата исключений, которые порождаются кодом, написанным на других языках (и которые поэтому могут не быть производными от System.Exception, компилятор C# отметит ошибку, если попробовать определить такой объект-исключение, но это не имеет значения для других языков программирования).

Полный синтаксис для try…catch…finally в C# выглядит следующим образом:

try {

// обычный код

} catch (MyException e) { // MyException выводится из System.Exception

// код обработки ошибки

}

// необязательные дополнительные блоки catch

finally {

// код очистки

}

Отметим, что блок finally является необязательным. Также допустимо не иметь блоков catch, в этом случае конструкция try…finally служит просто способом обеспечения, чтобы код в блоке finally всегда выполнялся, когда происходит выход из блока try. Это может быть полезно, например, если блок try содержит несколько инструкций return и требуется выполнить очистку ресурсов, прежде чем метод реально возвратит управление.

Указатели и небезопасный код

Указатели в C# используются почти таким же образом, как и в C++. Однако они могут объявляться и использоваться только в блоке небезопасного (ненадежного) кода. Любой метод можно объявить небезопасным (unsafe):

public unsafe void MyMethod() {

Можно альтернативно объявить любой класс или структуру небезопасными и:

unsafe class MyClass {

Объявление класса или структуры ненадежными означает, что все члены рассматриваются как ненадежные. Можно также объявить любое поле-член (но не локальные переменные) как ненадежное, если имеется поле-член типа указателя:

private unsafe int* рХ;

Можно также пометить блочный оператор как ненадежный следующим образом:

unsafe {

// инструкции, которые используют указатели

}

Синтаксис для объявления, доступа, разыменования и выполнения арифметических операций с указателями такой же, как и в C++:

// этот код будет компилироваться в C++ или C#

// и имеет одинаковый результат в обоих языках

int X = 10, Y = 20;

int *рХ = &Х;

*рХ = 30;

pХ = &Y;

++рХ; // добавляет sizeof(int) к рХ

Отметим, однако, следующие моменты.

□ В C# не допускается разыменовывать указатели void*, также нельзя выполнять арифметические операции над указателями void*. Синтаксис указателя void* был сохранен для обратной совместимости, для вызова внешних функций API, которые не знают о .NET и которые требуют указателей void* в качестве параметров.

□ Указатели не могут указывать на ссылочные типы (классы или массивы). Также они не могут указывать на структуры, которые содержат встроенные ссылочные типы в качестве членов. Это в действительности попытка защитить данные, используемые сборщиком мусора и средой выполнения .NET (хотя в C#, также как и в C++, если начать использовать указатели, почти всегда можно найти способ обойти любые ограничения, выполняя арифметические операции на указателях и затем разыменовывая их).

□ Помимо объявления соответствующих частей кода как ненадежных, необходимо также определять для компилятора флаг /unsafe при компиляции кода, который содержит указатели.

□ Указатели не могут указывать на переменные, которые встроены в ссылочные типы данных (например, членов класса), если только они не объявлены внутри инструкции fixed.

Фиксация донных в куче

Разрешается присвоить адрес типа данных значения указателю, даже если этот тип встроен как поле-член в ссылочный тип данных. Однако такой указатель должен быть объявлен внутри инструкции fixed. Причина этого в том, что ссылочные типы могут в любое время перемещаться в куче сборщиком мусора. Сборщик мусора знает о ссылках C# и может обновить их, как требуется, но он не знает об указателях. Следовательно, если указатель направлен на член класса в куче и сборщик мусора перемещает весь экземпляр класса, то будет указан неправильный адрес. Инструкция fixed не позволяет сборщику мусора перемещать указанный экземпляр класса во время выполнения блока fixed, гарантируя целостность значений указателей.

class MyClass {

public int X; // и т.д.

}

// где-то в другом месте кода

MyClass Mine = new MyClass(); // выполнить обработку

fixed (int *pX = Mine.X) {

// можно использовать рХ в этом блоке

}

Возможно вкладывание блоков fixed для объявления более одного указателя. Можно также объявить более одного указателя в одной инструкции fixed при условии, что оба указателя имеют один тип объекта ссылки.

fixed (int *рХ = Mine.X, *рХ2 = Mine2.X) {

Объявление массивов в стеке

C# предоставляет оператор stackalloc, который используется в соединении с указателями для объявления массива в стеке без накладных расходов. Массив, размещаемый таким образом, не является полным объектом System.Array в стиле C#, он является просто массивом чисел, аналогичным одномерному массиву C++. Элементы этого массива не инициализируются и доступны с помощью такого же синтаксиса, как и в C++, с использованием квадратных скобок для указателя.

Оператор stackalloc требует спецификации типа данных и числа размещаемых элементов.

Синтаксис C++:

unsigned long рМуArray[20];

Синтаксис C#:

ulong *pMyArray = stackalloc ulong[20];

Отметим, однако, что хотя эти массивы похожи, версия C# позволяет определить размер во время выполнения:

int X;

// инициализировать X

ulong *pMyArray = stackalloc ulong[X];

Интерфейсы

Интерфейсы являются особенностью C#, которая не имеет аналога в ANSI C++, хотя компания Microsoft ввела интерфейсы в C++ с помощью специального ключевого слова. Идея интерфейса развилась из интерфейсов COM, которые предназначены служить контрактом, который указывает, какие методы или свойства реализует объект.

Интерфейс в C# не совсем такой, как интерфейс COM, так как он не имеет связанного с ним GUID, не является производным из IUnknown и не имеет связанных с ним записей в реестре (хотя можно отобразить интерфейс C# на интерфейс COM). Интерфейс C# является просто множеством определений функций и свойств. Он может рассматриваться как аналог абстрактного класса и определяется с помощью синтаксиса аналогичного класса.

interface IMyInterface {

void MyMethod(int X);

}

Можно заметить, однако, следующие синтаксические различия с определением класса:

□ Методы не имеют модификаторов доступа.

□ Методы никогда не реализуются в интерфейсе.

□ Методы не объявляются виртуальными или явно абстрактными. Выбор методов принадлежит классу, который реализует этот интерфейс.

Класс реализует интерфейс, наследуя из него. Класс может быть производным только от одного класса, но от любого числа интерфейсов. Если класс реализует интерфейс, то он должен предоставить реализации всех методов, определенных этим интерфейсом.

class MyClass : MyBaseClass, IMyInterface, IAnotherInterface // и т.д.

{

public virtual void MyMethod(int X) {

// реализация

}

// и т.д.

В этом примере выбрана реализация MyMethod как виртуального метода с открытым доступом.

Интерфейсы могут также выводиться из других интерфейсов, и в этом случае производный интерфейс содержит свои собственные методы, а также методы базового интерфейса.

interface IMyInterface : IBaseInterface

Можно проверить, что объект реализует интерфейс, используя либо оператор is, либо оператор as для преобразования его в этот интерфейс. Альтернативно можно преобразовать его напрямую, но в этом случае будет получено исключение, если объект не реализует интерфейс, поэтому этот подход годится только в том случае, если известно, что преобразование пройдет успешно. Можно использовать полученную таким образом ссылку на интерфейс, чтобы вызывать методы на этом интерфейсе (реализация будет предоставляться экземпляром класса).

IMyInterface MyInterface;

MyClass Mine = new MyClass();

MyInterface = Mine as IMyInterface;

if (MyInterface != null) MyInterface.MyMethod(10);

Основные применения интерфейсов следующие:

□ Взаимодействовать и устанавливать обратную совместимость с компонентами COM.

□ Служить в качестве контракта для других классов .NET. Интерфейс может использоваться для указания, что класс реализует некоторые свойства. Например, цикл C# foreach работает внутренне, проверяя, что класс, в котором он используется, реализует интерфейс IEnumerate, и вызывая затем методы, определенные этим интерфейсом.

Делегаты

Делегаты в C# не имеют прямого эквивалента в C++ и выполняют ту же самую задачу, что и указатели на функции в C++. Идея делегата состоит в том, что указатель на метод помещается в специальный класс вместе со ссылкой на объект, на котором вызывается метод (для метода экземпляра или со ссылкой null для статического метода). Это означает, что в отличие от указателя на функцию в C++, делегат C# содержит достаточно информации для вызова метода экземпляра.

Формально делегат является классом, который выводится из класса System.Delegate. Следовательно, создание экземпляра делегата включает два этапа: определение этого производного класса и объявление переменной соответствующего типа. Определение класса делегата включает данные полной сигнатуры (с возвращаемым типом) метода, который содержит делегат.

Основное использование делегатов состоит в передаче и вызове ссылок на методы: ссылки на методы нельзя передавать непосредственно, но они могут передаваться внутри делегата. Делегат обеспечивает безопасность типа данных, не позволяя вызывать метод с неверной сигнатурой. Метод, который содержит делегат, может вызываться синтаксически как вызов делегата. Следующий код показывает общие принципы. Первое, необходимо определить класс делегата:

// определить класс делегата, который представляет метод,

// получающий int и возвращающий void

delegate void MyOp(int X);

Затем, для целей этого примера объявим класс, который содержит вызываемый метод:

// затем определение класса

class MyClass {

void MyMethod(int X) {

// и т.д.

}

}

Еще позже, может быть при реализации некоторого другого класса, имеется метод, которому должна быть передана ссылка на метод с помощью делегата:

void MethodThatTakesDelegate(MyOp Op) {

// вызвать метод, передавая ему значение 4

Oр(4);

}

// и т.д.

И, наконец, код, который реально использует делегата:

MyClass Mine = new MyClass();

// Создать экземпляр делегата MyOp. Настроить его,

// чтобы он указывал на метод MyMethod из Mine.

MyOp DoIt = new MyOp(Mine.MyMethod);

После объявления переменной делегата можно вызвать метод с помощью делегата:

DoIt();

Или передать его в другой метод:

MethodThatTakesDelegate(DoIt);

В частном случае, когда делегат представляет метод, который возвращает void, этот делегат является широковещательным делегатом и может одновременно представлять более одного метода. Вызов делегата заставляет все методы вызываться по очереди. Можно использовать операторы + и += для добавления метода делегату, а - и -= — для удаления метода, который уже находится в делегате. Делегаты рассматриваются более подробно в главе 6.

События

События являются специальной формой делегатов, которые используются для поддержки модели уведомления о событии с помощью обратного вызова. Событие имеет следующую сигнатуру:

delegate void EventClass(obj Sender, EventArgs e);

Это сигнатура, которую должен иметь любой обработчик событий с обратным вызовом. Ожидается, что Sender будет ссылкой на объект, который инициирует событие, в то время как System.EventArgs (или любой класс, производный из EventArgs, который также допустим в качестве параметра) является классом, используемым средой выполнения .NET для передачи базовой информации, имеющей отношение к деталям события.

Для объявления события используется специальный синтаксис:

public event EventClass OnEvent;

Клиенты используют синтаксис += широковещательных делегатов для информирования, что они хотят получить уведомление.

// EventSource ссылается на экземпляр класса, который содержит событие

EventSource.OnEvent += MyHandler;

Источник просто вызывает событие, когда потребуется, используя такой же синтаксис, который был показан выше для делегатов. Так как событие является широковещательным делегатом, то все обработчики событий будут вызваны в ходе этого процесса. События рассматриваются более подробно в главе 6.

OnEvent(this, new EventArgs());

Атрибуты

Концепция атрибутов не имеет эквивалента в ANSI C++, однако атрибуты поддерживаются компилятором Microsoft C++ как специальное расширение Windows. В версии C# имеются классы .NET, которые выводятся из System.Attribute. Они могут применяться к различным элементам кода C# (классам, перечислениям, методам, параметрам и т.д.) для создания дополнительной документирующей информации в компилированной сборке. Кроме того, некоторые атрибуты распознаются компилятором C# и будут иметь влияние на компилированный код. Они включают следующие:

Атрибут

Описание

DllImport

Указывает, что метод определен во внешней DLL.

StructLayout

Позволяет расположить содержимое структуры в памяти. Позволяет получить эквивалент union в C#.

Obsolete

Создает ошибку компилятора или предупреждение, если используется этот метод.

Conditional

Заставляет выполнить условную компиляцию. Этот метод и все ссылки на него будут игнорироваться, если присутствует определенный символ препроцессора.

Существует большое число других атрибутов, а также возможно задать свои собственные специальные атрибуты. Использование атрибутов рассматривается в главах 6 и 7.

Согласно синтаксису атрибуты указываются непосредственно перед объектом, к которому они применимы, в квадратных скобках. Это такой же синтаксис, как у атрибутов Microsoft C++.

[Conditional("Debug")]

void DisplayValuesOfImportantVariables() {

// и т.д.

Директивы препроцессора

C# поддерживает директивы препроцессора таким же образом, как C++, за исключением того, что их значительно меньше. В частности, C# не поддерживает обычно используемую директиву C++ #include. (Она не требуется, так как в C# не используется предварительное объявление.)

Синтаксис директив препроцессора в C# такой же, как в C++. В C# поддерживаются следующие директивы:

Директива

Значение

#define/#undef

Так же как в C++, за исключением того, что они должны появиться в начале файла, до кода C#.

#if/#elif/#else/#endif

То же самое, что в C++ #ifdef/#elif/#else/#endif.

#line

То же самое, что в C++ #line.

#warning/#error

То же самое, что в C++ #warning/#error.

#region/#endregion

Помечает блок кода как область. Области распознаются некоторыми редакторами (такими, как редактор VisualStudio.NET) и поэтому могут использоваться для улучшения компоновки кода, представленного пользователю при редактировании.

Пpиложение B

C# для разработчиков Java

В "Искусстве войны" Сунь Цзы утверждает, что "необходимо рассматривать вещи большой важности с небольшим усилием, а вещи небольшой важности с большим усилием". Это может звучать странно, но автор хочет, видимо, сказать, что если заботиться о незначительных вещах, то важные вещи тогда позаботятся о себе сами. Как это применимо к C# и Java?

При первом взгляде на код C# может показаться, что он не слишком впечатляющий, так как обнаруживается отчетливое сходство между ним и Java. Однако, если вы ожидали каких-то существенных изменений, то правда состоит в том, что на самом деле существует не так уж много синтаксических различий. Пути двух языков расходятся во внутренних тонкостях таких вещей, как перегрузка операторов, индексаторы, делегаты, свойства и перечисления с контролем типов. При более внимательном рассмотрении можно понять, что в конце концов между ними существует большое различие. (Приведенные выше темы будут подробнее рассмотрены позже в этом приложении.)

В этом приложении мы сосредоточимся прежде всего на вопросе, важном для разработчиков Java: как можно использовать в C# опыт использования Java, а также подчеркнем свойства присущие C#, и рассмотрим, что C# делать не может. Здесь предполагается, что читатель хорошо знаком с Java, поэтому отсутствует подробное описание языка Java, за исключением некоторых существующих различий.

Основы

Одно из основных различий между C# и Java лежит не в самом языке, а в платформе, поверх которой они реализованы. Программам Java требуется для выполнения кода рабочая среда времени выполнения Java Runtime Environment. C# и, на самом деле, вся платформа .NET выполняются в среде Common Language Runtime.

Большинство свойств CLR, внутреннее управление памятью, согласованность среды, масштабируемость и независимость от базовой платформы отражены в JRE Java. В то время, как JRE ограничена исключительно одним языком Java, CLR предоставляет поддержку и интеграцию нескольких языков с помощью VOS (virtual object system — система виртуальных объектов), которая предоставляет богатую типами систему, предназначенную для реализации множества различных типов языков программирования. Исходный код Java можно компилировать в промежуточное состояние, называемое байт-кодом. Он может затем выполняться с помощью поставляемой виртуальной машины. CLR, наоборот, не предоставляет виртуальную машину. Код C# также компилируется в промежуточное состояние, называемое для удобства промежуточным языком (IL, Intermediate Language). Но код IL передается в управляемые CLR процессы выполнения или компиляторам JIT CLR, обычно называемым JITters, которые преобразуют по требованию разделы IL в собственный код.

Давайте рассмотрим известный пример "Hello, World!" на Java (который будет показан здесь без затенения):

public class Hello {

public static void main(String args[]) {

System.out.println("Hello world! This is Java Code!");

}

}

Соответствующий код C# для этого примера следующий (представлен на сером фоне):

public class Hello {

public static void Main(string[] args) {

System.Console.WriteLine("Hello world! This is C# code!");

}

}

Прежде всего можно заметить, что эти два кода очень похожи, по большей части различия незначительны (такие, как использование заглавных букв в string и Main и использование System.Console.WriteLine вместо System.out.println). Он является, с другой стороны, по-прежнему зависимым от регистра символов.

Необходимо отметить тип string, который в C# также может записываться с заглавной S — как String. Следовательно, приведенный выше код можно записать в следующем виде:

public class Hello {

public static void Main(String [] args) {

System.Console.WriteLine("Hello world! This is C# code!");

}

}

Можно заметить, что спецификатор ранга массива [] перемещен из позиции перед переменной args в примере Java в положение между типом string и переменной args в примере C#. В C# спецификатор ранга массива должен появляться перед именем переменной, так как массив является на самом деле типом данных, что указывается с помощью []:

// C#

int [] X; // массив целых чисел в C#

// пример java

int [] х; // массив целых чисел в java

int x[]; // массив целых чисел в java

Тот же самый код C# можно также представить следующим образом:

using System;

class Hello {

public static int Main() {

Console.WriteLine ("Hello world! This is C# code!");

return 0;

}

}

Как можно видеть, изменилось несколько элементов. Объявление string [] args в сигнатуре метода является в C# необязательным (хотя скобки должны стоять перед именем параметра), как и использование public при объявлении метода. Ключевое слово using аналогично по смыслу ключевому слову include в Java, и так как System является используемым по умолчанию включением в C#, первая строка позволяет нам опустить System, которое находилось перед Console.WriteLine. Поскольку мы используем в этом примере int вместо void, необходимо включить строку return 0;.

Блоки кода C# заключаются в фигурные кавычки, также как в Java. Можно сказать, что метод Main() является частью класса, так как он заключен в фигурные кавычки. Точкой входа в приложении C# является метод static Main, как требует компилятор. Необходимо отметить, что Java использует main() в нижнем регистре. Подчеркнем также, что только один класс в приложении может иметь Main. Модификатор доступа public (обсуждаемый позднее) объявляет метод доступным потребителям кода вне класса, пакета или приложения, и также, как и компилятор, требует сделать метод видимым.

Аналогично в Java ключевое слове static позволяет вызывать метод сначала без создания экземпляра класса. Для метода Main() можно выбрать в качестве возвращаемого типа значения void или int. void определяет, что метод не возвращает значение, a int определяет, что он возвращает целое значение.

Идентификаторы

Ключевые слова, рассматриваемые в следующем разделе, не могут служить идентификаторами ни в Java, ни в C#, однако в C# можно использовать ключевые слова как идентификаторы, помещая перед ними символ @. Отметим, что это исключение имеет отношение только к ключевым словам и не нарушает другие правила. Оба языка являются зависимы ми от регистра символов, поэтому идентификаторы должны иметь согласованное использование заглавных букв. Хотя идентификаторы могут содержать буквы и цифры, первый символ идентификатора как в C#, так и в Java не должен быть цифрой. Java не допускает никаких символов кроме $, а C# вообще не допускает никаких символов:

int 7х; // неверно, цифра не может начинать идентификатор

int х7; // верно, цифра может быть частью идентификатора

int х; // верно

int х$; // неверно, никакие символы недопустимы

int @7k; // неверно, @ работает только для ключевых слов

int @class; // верно, @ перед ключевым словом позволяет использовать

// его в качестве идентификатора

Стандарты именования

Одним из основных различий, которое может быть не очевидно на первый взгляд, и которое не связано специально с языком C#, является синтаксис записи идентификаторов. Java практикует обозначения типа camel, означающее, что методы и идентификаторы используют меленькую букву для первой буквы имени и заглавную букву для первой буквы любого другого слова в имени. Общий синтаксис, которому следуют большинство программистов в Java, представлен ниже:

int id;

int idName;

int id_name; // также используется

final int CONSTANT_NAME; // широко распространен

int reallyLongId;

public class ClassName; // каждая первая буква заглавная

public interface _InterfaceName; // с предшествующим подчеркиванием

public void method(){}

public void methodName(){}

public void longMethodName(){}

public void reallyLongMethodName(){}

На основе библиотеки классов, предоставленной компанией Microsoft для C#, можно сделать некоторые предположения о стандартах наименований в C#. Документированные рекомендации по именованию для C# не были представлены в то время когда писалась эта книга. Каждая первая буква идентифицирующих имен всех методов и свойств будет заглавной, так же как и каждая первая буква имен всех классов и пространств имен (рассматриваемых позже). Интерфейсы используют в качестве первого символа I. Некоторые примеры приведены ниже:

int id;

int idName;

public class ClassName // каждая первая буква заглавная

public interface IInterfaceName // имени интерфейса предшествует I

public void Method(){} // первая буква всегда заглавная

public void MethodName(){} // первая буква всех других слов

// будет заглавная

public void LongMethodName(){}

public void ReallуLongMetodName(){}

Ключевые слова

Как известно, ключевое слово является специальным зарезервированным словом языка. Мы уже встречали некоторые из них допустим, объявление переменной как целого числа с помощью int. Другими примерами ключевых слов являются public, class, static и void в листингах кода в этом приложении.

Ключевые слова можно разделить на ряд категорий в связи с их назначением. В этом разделе мы выделим и определим каждую категорию, а также идентифицируем ключевые слова. Реальные ключевые слова будут идентифицироваться своими версиями в Java, чтобы можно было легко их находить. Затем будет дан эквивалент C# (если существует). Для тех ключевых слов, которые присутствуют только в Java, будет предоставлено лучшее соответствие. Ключевые слова, представленные в C#, но не в Java, будут даны в своей собственной категории с лучшим приблизительным эквивалентом в Java (если такой существует).

Простейшие ключевые слова: byte, char, short, int, long, float, double и boolean

Примитивные типы данных в обоих языках ссылаются на низкоуровневые типы значений языка. Конечно, диапазон значений указанных типов может различаться в том или другом языке. Логические значения в C# идентифицируются ключевым словом bool в противоположность boolean в Java. Ниже представлен табличный список типов данных Java и их аналогов в C#:

Тип Java

Описание

Эквивалентный тип C#

Описание

byte

8-битовое со знаком

sbyte

8-битовое со знаком

short

16-битовое со знаком

short

16-битовое со знаком

int

32-битовое со знаком

int

32-битовое со знаком

long

64-битовое со знаком

long

64-битовое со знаком

float

32-битовое число с плавающей точкой со знаком

float

32-битовое число с плавающей точкой со знаком

double

64-битовое число с плавающей точкой со знаком

double

64-битовое число с плавающей точкой со знаком

boolean

true/false

bool

true/false

char

2-байтовый Unicode

char

2-байтовый Unicode

Существует также ряд типов, поддерживаемых C#, которые Java не использует. Таблица ниже выделяет эти типы данных.

Уникальный тип данных C#

Описание

Byte

8-битовое целое без знака

ushort

16-битовое целое без знака

Uint

32-битовое целое без знака

ulong

64-битовое целое без знака

decimal

128-битовое

Ключевые слова-переменные: this, void и super

Эти ключевые слова сами являются переменными. Оба языка, Java и C#, имеют по три ключевых слова, которые попадают в эту категорию. Ключевые слова this и void обладают в обоих языках одинаковой функциональностью.

super — эта ссылочная переменная используется для указания класса-предка. В C# эквивалентом является base. Возьмем класс Power, который предоставляет возможность найти степень заданного числа и степень, в которую требуется возвести (при условии, что не происходит переполнение):

public class SuperEX {

int power;

public SuperEX(int power) {

this.power = power;

}

public int aMethod(int x) {

int total = 1;

for (int i = 0; i < power; i++) {

total *= x;

}

return total;

}

public static void main(String args[]) {

SuperEX x = new SuperEX(Integer.parseInt(args[0]));

int tot = x.aMethod(Integer.parseInt(args[1]));

System.out.println(tot);

}

}

Класс-потомок этого класса сможет получить доступ к методу aMethod с помощью вызова super.aMethod(<int value>), к переменной power — с помощью вызова super.power = <int value>, и даже к конструктору — с помощью вызова super(<int value>), где <int value> может быть любым целым литералом, переменной или константой.

Аналогично в C# класс-потомок этого класса сможет получить доступ к методу aMethod с помощью вызова super.aMethod(<int value>) и к переменной power — с помощью вызова super.power = <int value>. Сделать вызов базового конструктора тоже возможно, синтаксис, однако, будет отличаться. Пример ниже является эквивалентом в C# для SuperEX:

namespace SuperEX {

using System;

public class SuperEX {

internal int power;

public SuperEX(int power) {

this.power = power;

}

public int aMethod(int x) {

int total = 1;

for (int i = 0; i < power; i++) {

total *= x;

}

return total;

}

public static void Main(String [] args) {

SuperEX x = new SuperEX(int.Parse(args[0]));

int tot = x.aMethod(int.Parse(args[1]));

Console.WriteLine(tot);

}

}

public class Child: SuperEX {

public Child() : base(55) { }

}

}

Как можно видеть на примере класса-потомка Child, вызов конструктора базового класса является частью объявления конструктора класса-потомка. Программист может по своему усмотрению определить список параметров конструктора класса-потомка, но ссылка на конструктор базового класса должна соответствовать списку аргументов, требуемых базовым классом. В данном примере конструктор потомка может получить форму <child constructor>: base constructor(<int value>), где <int value> может быть любым целым литералом, переменной или константой, a <child constructor> представляет любой конструктор потомка, который хочет воспользоваться конструктором базового класса. Более общая версия, как получить доступ к конструктору базового класса, представлена ниже:

ChildConstructor(argument_list) : BaseConstructor(argument_list)

Ключевые слова управления пакетами: import и package

Так же как в Java, в C# инструкции import предоставляют доступ к пакетам и классам в коде без полной квалификации, директива using может использоваться для того, чтобы сделать компоненты пространства имен видимыми в классе без полной квалификации. В C# не существует эквивалента инструкции package. Чтобы сделать класс частью пространства имен, надо поместить его в объявление пространства имен. Пространства имен будут обсуждаться более подробно позже в этой главе.

Ключевые слова управления потоком выполнения и итерациями: break, case, continue, default, do, else, for, if, instanceof, return, switch и while

Большинство упомянутых выше ключевых слов имеют одинаковые имена, синтаксис и функциональность в C# и Java. Исключением является оператор Java instanceof, используемый для определения того, что объект является экземпляром класса или некоторого подкласса этого класса. C# предоставляет такую же функциональность с помощью ключевого слова is. Некоторые примеры того, как инструкции работают в C#, даны ниже. Можно видеть, что большая часть кода точно такая же, как и в Java:

public static void Main (string[] args)

int option = int.Parse(arg[0]);

if (option == 1) {

// что-нибудь сделать

}

else if (option == 2) {

// сделать что-нибудь еще

}

switch (option) {

case 1:

// сделать что-нибудь

break;

case 2:

// сделать что-нибудь еще

default:

break;

}

}

C# вводит инструкцию foreach, используемую специально для перебора без изменения элементов коллекции или массива, чтобы получить желаемую информацию. Изменение содержимого может иметь непредсказуемые побочные эффекты. Инструкция foreach обычно имеет форму, показанную ниже:

foreach(ItemType item in TargetCollection)

ItemType представляет тип данных, хранящихся в коллекции или массиве, a TargetCollection представляет реальный массив или коллекцию. Существует два набора требований, которым должна удовлетворять коллекция, перебор элементов которой будет выполняться с помощью инструкции foreach. Первый набор имеет отношение к составу самой коллекции. Это следующие требования:

□ Тип коллекции должен быть интерфейсом, классом или структурой.

□ Тип коллекции должен включать метод GetEnumerator() для возврата типа перечислителя. Тип перечислителя является по сути объектом, который позволяет перебрать коллекцию элемент за элементом.

Второй набор требований имеет дело с составом типа перечислителя, возвращаемого упомянутым выше методом GetEnumerator(). Список требований дан ниже:

□ Перечислитель должен предоставить метод MoveNext() типа boolean.

□ MoveNext() должен возвращать true, если в коллекции есть еще элементы.

□ MoveNext() должен увеличивать счетчик элементов при каждом вызове.

□ Тип перечислителя должен предоставлять свойство с именем Current, которое возвращает ItemType (или тип, который может быть преобразован в ItemType).

□ Метод доступа свойства должен возвращать текущий элемент коллекции.

Следующий пример использует foreach для просмотра таблицы Hashtable:

Hashtable t = new Hashtable();

t["a"] = "hello";

t["b"] = "world";

t["c"] = "of";

t["d"] = "c-sharp";

foreach (DictionaryEntry b in t) {

Console.WriteLine(b.Value);

}

Ключевые слова модификации доступа: private, protected, public, и (пакет по умолчанию)

Ключевое слово private используется, чтобы сделать методы и переменные доступными только изнутри элементов содержащего их класса. Функциональность одинакова в обоих языках, модификатор public позволяет сущностям вне пакета получить доступ к внутренним элементам. Конечно, для C# это будут сущности вне пространства имен, а не пакета.

C# и Java различаются в том, как обрабатываются protected и 'default'. В то время как в Java protected делает метод или переменную доступной для классов в том же пакете или подклассах класса, protected в C# делает код видимым только для этого класса и подклассов, которые от него наследуют.

C# вводит также новый модификатор доступа — internal. Ключевое слово internal изменяет члены данных так, что они будут видимы всему коду внутри компонента, но не клиентам этого компонента. Различие здесь между модификатором в Java, который указывает элемент, доступный только для элементов в пакете, и internal состоит в том, что internal доступен всем элементам сборки, которая может охватывать несколько пространств имен. Сборки и пространства имен будут рассмотрены позже в этом приложении.

Модификаторы: abstract, class, extends, final, implements, interface, native, new static, synchronized, transient, volatile

Модификатор abstract имеет одну и ту же форму и синтаксис в обоих языках. Таким же является ключевое слово class. C# не имеет модификаторов extends или implements. Чтобы вывести из класса или реализовать интерфейс, используйте оператор :. Когда список базового класса содержит базовый класс и интерфейсы, базовый класс следует первым в списке. Ключевое слово interface используется для объявления интерфейса. Примеры рассмотренных ранее концепций приведены ниже:

class ClassA: BaseClass, Iface1, Iface2 {

// члены класса

}

public interface IfruitHaver {

public void Fruit();

}

public class plant: IfruitHaver {

public Plant() {

}

public void Fruit() {

}

}

class Tree : Plant {

public Tree() {

}

}

Ключевое слово final в Java трудно отнести к какой-то категории. Частично причина заключается в том, что оно предоставляет вид функциональности два-в-одном, что делает трудным соединение его с каким-либо одним назначением. Объявление класса как final запечатывает его, делая невозможным расширение. Объявление метода как final также его запечатывает, делая невозможным его переопределение. Объявление переменной как final является по сути объявлением ее только для чтения. Именно для чтения, а не константой, так как возможно задать значение final как значение переменной. Значения констант должны быть известны во время компиляции, поэтому константы могут задаваться равными только другим константам.

В противоположность этому C# предоставляет специфические приемы для рассмотрения каждого отдельного вопроса. По умолчанию подкласс не должен иметь возможности повторно реализовывать открытый метод, который уже реализован в суперклассе. C# вводит новую концепцию — сокрытие метода, что позволяет программисту переопределить члены суперкласса в классе-наследнике и скрыть реализацию базового класса, C# использует в данном случае модификатор new.

Это делается присоединением new к объявлению метода. Достоинство сокрытия версий членов базового класса состоит в том, что можно выборочно определить, какую реализацию использовать. Пример такой концепции представлен в коде ниже:

namespace Fona {

using System;

public class Plant {

public Plant(){}

public void BearFruit() {

Console.WriteLine("Generic plant fruit");

}

}

class Tree : Plant {

public Tree(){}

// ключевое слово new используется здесь явно, чтобы скрыть

// базовую версию

new public void BearFruit() {

Console.WriteLine("Tree fruit is:->Mango");

}

}

public class PlantEX {

public PlantEX(){}

public static void Main(String[] args) {

Plant p = new Plant();

p.BearFruit(); // вызывает реализацию базового класса

Tree t = new Tree();

t.BearFruit(); // вызывает реализацию класса наследника

((Plant)t).BearFruit(); // вызывает реализацию базового класса,

// используя наследника.

}

}

}

Выполнение этого примера создает следующий вывод:

Generic plant fruit

Tree fruit is:->Mango

Generic plant fruit

Необходимо отметить, что существует различие между сокрытием метода и обыкновенным полиморфизмом. Полиморфизм всегда будет предоставлять для вызова метод класса-наследника.

Примечание. Во время написания этой книги автор обнаружил, что сокрытие метода компилируется без ошибок и предупреждений, даже когда ключевое слово new не было использовано.

Чтобы предоставить функциональность переопределения, используются модификаторы virtual и override. Все методы в базовом классе, которые будут переопределяться, должны применить ключевое слово virtual. Чтобы реально переопределить их, в классе-наследнике используется ключевое слово override. Ниже представлен пример класса Tree, измененный для вывода переопределенной функциональности:

class Tree Plant {

public Tree() {}

public override void Fruit() {

Console.WriteLine("Tree fruit is:->Mango");

}

}

Компиляция и выполнение этого создают следующий вывод.

Generic plant fruit

Tree fruit is:->Mango

Tree fruit is:->Mango

Как можно видеть, вызывается самый последний переопределенный метод Fruit() независимо от использования cтратегии преобразования ((Plant)t).BearFruit(), которая применялась ранее для ссылки на метод Fruit() базового класса. Модификатор new может также использоваться для сокрытия любого другого типа наследованных из базового класса членов аналогичной сигнатуры.

Чтобы помешать случайному наследованию класса, используется ключевое слово sealed. В приведенном выше призере можно изменить объявление Plant на public sealed class Plant и в этом случае Tree больше не сможет от него наследовать.

C# не имеет модификатора native. В Java использование native указывает, что метод реализован на зависимом от платформы языке. Это требует чтобы метод был абстрактным, так как реализация должна находиться в другом месте. Ближайшим к этому типу функциональности является модификатор extern. Использование extern предполагает, что код реализуется вовне (например, некоторой собственной DLL). Однако в отличие от Java, нет необходимости использовать ключевое слово abstract в соединении с ним. Фактически это приведет к ошибке, так как они означают две похожие, но различные вещи. Ниже класс Plant из предыдущего примера показывает, как можно использовать extern:

public class Plant : IfruitHaver {

public extern int See();

public Plant(){}

public void Fruit() {

Console.WriteLine("Generic plant fruit");

}

}

Это не имеет большого смысла без использования атрибута DllImport для определения внешней реализации. Более подробно атрибуты будут рассмотрены позже в приложении. Дальнейший код делает соответствующие изменения, предполагая, что функция See экспортирована ресурсом User32.dll:

public class Plant: IfruitHaver {

[System.Runtime.InteropServices.DllImport("User32.dll)]

public static extern int See();

public Plant(){}

public void Fruit() {

Console.WriteLine("Generic plant fruit");

}

}

Здесь метод See() помечен как статический. Атрибут DllImport требует этого от методов, на которых он используется

Пока не существует версии C# ключевых слов transient, volatile или synchronized. Однако существует ряд способов, предоставляемых SDK .NET для имитации некоторой их функциональности. C# использует атрибут NonSerialized, связанный с полями класса, для предоставления механизма, аналогичного по функциональности модификатору Java transient, этот атрибут однако опротестован, поэтому может быть изменен в будущих версиях.

Синхронизация в C# несколько усложнена (более трудоемка) по сравнению с ее аналогом в Java. В общем, любой поток выполнения может по умолчанию получить доступ ко всем членам объекта. Имеется, однако ряд способов синхронизации кода в зависимости от потребностей разработчика с помощью использования таких средств, как Monitors. Они предоставляют возможность делать и освобождать блокировки синхронизации на объектах SyncBlocks, которые содержат блокировку используемую для реализации синхронизированных методов и блоков кода; список ожидающих потоков выполнения, используемых для реализации функциональности монитора ReaderWriterLock, который определяет образец одного писателя для многочисленных читателей; примитивы синхронизации Mutex, предоставляющие межпроцессную синхронизацию; и System.Threading.Interlocked, который может использоваться для предоставлении синхронизированного доступа к переменным через несколько потоков выполнения.

Первый шаг к синхронизации в C# состоит в ссылке на сборку System.EnterpriseServices.dll. Инструкция lock(<expression>) {// блок кода} является единственным, связанным с синхронизацией, ключевым словом в C#. Оно может применяться, также как в Java, для получения взаимно исключающего доступа к блокировке объекта <ref>. Все попытки получить доступ к <expression> будут блокированы, пока поток выполнения с блокировкой не освободит ее. Обычно используется выражение либо this, либо System.Type, соответствующее Type представленного объекта. Их использование будет защищать переменные экземпляра выражения в то время, как использование System.Type будет защищать статические переменные.

Ключевые слова обработки ошибок: catch, finally, throw, throws, try

Эти модификаторы являются одинаковыми в обоих языках, за исключением инструкции throws, которая отсутствует в C#. Пугающая вещь в отношении инструкции throws из Java состоит в том, что она позволяет потребителям компонента с помощью относительно простого синтаксиса использовать компонент, не зная какие исключения он может порождать. Можно удовлетвориться заверениями, что компилированный код обрабатывает все, имеющие отношение к делу, исключения, так как компилятор будет иначе отказывать и информировать обо всех не перехваченных исключениях. Функциональность такого рода отсутствует в C# в настоящее время. Предоставление метода потребителям сборки, желающем знать, когда порождаются исключения, должно будет привести к хорошей практике документирования или некоторому умелому программированию атрибутов.

Выполнение вычислений может привести к сценарию, где вычисленный результат выходит за границы диапазона типа данных переменной результата. В Java, если целые значения достигают своих пределов, то они имеют неприятную особенность переходить к противоположной границе. Чтобы проиллюстрировать это, рассмотрим код следующего класса:

// OverflowEX.java

public class OverfTowEX {

publiс static void main(String args []) {

byte x = 0;

for (int i = 0; i < 130; i++) {

x++;

System.out.println(x);

}

}

}

Как известно, byte в Java является 8-битовым типом данных со знаком. Это означает, что диапазон значений byte лежит от -128 до 128. Результатом добавления единицы к любой границе заданного диапазона целого типа будет другая граница диапазона целого типа. Поэтому в этом примере добавление 1 к 127 создаст -128. И если откомпилировать и выполнить эту программу, то последние пять чисел выведенные на консоли будут следующими:

126

127

-128

-127

-126

Это может оказаться весьма существенной проблемой, особенно в связи с тем, что ни предупреждение ни исключение не порождаются, чтобы позволить обработать такое событие (возможно, сохраняя значение в типе с большим диапазоном значений). По умолчанию C# также обрабатывает ситуации переполнения, но язык и компилятор предоставляют инструменты для явной обработки и уведомления программиста в случае переполнения.

□ Программный подход. Чтобы бороться с этим типом молчаливых ошибок, C# вводит концепцию проверяемых и непроверяемых инструкций. Ключевое слово checked используется для управления контекстом проверки переполнения в случае операций и преобразований арифметики целого типа, таких, как представленные выше. Оно может использоваться как оператор или как инструкция. Инструкции checked/unchecked следуют приведенному ниже синтаксису (i). Они предназначены для помещения в скобки ряда инструкций, которые могут создавать переполнение. Синтаксис операции checked/unchecked показан в пункте (ii). Операция checked проверяет переполнение одиночных выражений:

(i) checked {block_of_code}

unchecked { block_of_code}

(ii) checked (expression)

unchecked (expression)

block_of_code содержит код, в котором инструкция checked/unchecked наблюдает за переполнением, a expression представляет выражение, в котором checked/unchecked наблюдает за переполнением в конечном значении. Показанный далее пример иллюстрирует использование checked/unchecked:

// OverflowEX.cs

public class OverflowEX {

public static void Main(String() args) {

sbyte x = 0; // помните, что необходимо изменить byte на sbyte

for (int i = 0; i < 130; i++) {

checked {

// можно также использовать checked(x++)

x++;

Console.WriteLine(x);

}

}

}

}

□ Подход с ключом компилятора. Для контроля переполнения во всем приложении может использоваться настройка компилятора /checked+. Чтобы проиллюстрировать это, удалим инструкцию checked из приведенного выше примера и попытаемся компилировать его, используя флаг /checked+. С его помощью можно включать и выключать проверку арифметического переполнения, изменяя состояние конфигурационных свойств на странице свойств проекта. Задание значения как true будет включать проверку переполнения.

При включенной проверке переполнения можно обсудить использование инструкции unchecked. По сути она предоставляет функциональность для произвольного исключения проверки выражений или блоков инструкций в то время, когда включена проверка во всем приложении. В примере ниже предыдущая инструкция заменяется инструкцией unchecked. Компиляция и выполнение этого кода будет создавать вывод, аналогичный выводу OverflowEX.java.

// OverflowEX.сs.

public class OverflowEX {

public static void Main(String[] args) {

sbyte X = 0;

for (int i = 0; i < 130; i++) {

unchecked { // можно также использовать unchecked(x++)

x++;

Console.WriteLine(x);

}

}

}

}

Входные и выходные данные

Возможность собрать входные данные из командной строки и вывести данные в командной строке является интегральной частью функциональности ввода/вывода в Java. Обычно в Java необходимо создать экземпляр объекта java.io.BufferedReader, используя поле System.in, чтобы извлечь ввод из командной строки. Ниже представлен простой класс Java — JavaEcho, который получает ввод с консоли и выводит его обратно, чтобы проиллюстрировать использование пакета Java.io для сбора и форматирования ввода и вывода:

// JavaEcho.java

import java.io.*;

public class JavaEcho {

public static void main(String[] args) throws IOException {

BufferedReader stdin = new BufferedReader(new InputSreamReader(System.in));

String userInput = stdin.readLine();

System.out.println("You said: " + userInput);

}

}

Класс System.Console предоставляет методы в C#, которые дают аналогичную функциональность для чтения и записи из и в командную строку, Нет необходимости в каких-либо дополнительных объектах, класс Console предоставляет методы, которые могут читать целые строки, читать символ за символом и даже показывать описанный выше поток, из которого выполняется чтение. Важно отметить, что эту функциональность дает System.Console без создания экземпляра объекта Console. Фактически можно обнаружить, что невозможно создать экземпляр объекта Console. Члены класса Console кратко описаны в таблицах ниже:

Открытые статические свойства (общие)

Описание

Error

Получает стандартный выходной поток ошибок системы

In

Получает стандартный входной поток ошибок системы

Out

Получает стандартный поток вывода системы

Открытые статические методы (общие)

Описание

OpenStandardError

Перезагруженный. Возвращает стандартный поток ошибок.

OpenStandardInput

Перезагруженный. Возвращает стандартный поток ввода.

OpenStandardOutput

Перезагруженный. Возвращает стандартный поток вывода.

Read

Читает следующий символ из стандартного потока ввода.

ReadLine

Читает следующую строку символов из Console.In, который по умолчанию задается как стандартный поток ввода системы.

SetError

Перенаправляет свойство Error для использования указанного потока TextWriter.

SetIn

Перенаправляет свойство In для использования указанного потока TextReader.

SetOut

Перенаправляет свойство Out для использования указанного потока TextWriter.

Write

Перезагруженный. Записывает указанную информацию в Console.Out.

WriteLine

Перезагруженный. Записывает информацию, за которой следует конец строки в Console.Out.

Как можно видеть, все члены Console являются статическими. static является примером модификатора C#. Он обладает тем же значением, что и его аналог в Java, т.е. делает указанную переменную или метод принадлежащим всему классу, а не одному какому-то экземпляру класса. Мы обсудим модификаторы более подробно позже в этом приложении.

С помощью мощных методов из класса Console можно записать эквивалент класса JavaEcho на C# следующим образом:

class CSEchoer {

static void Main(string[] args) {

string userInput = System.Console.ReadLine();

System.Console.WriteLine("You said : " + userInput);

}

}

Приведенный выше код значительно короче и легче, чем его аналог на Java. Статический метод Console.WriteLine предоставляет одну полезную вещь, а именно, возможность использовать форматированные строки. Гибкость форматированных строк может быть проиллюстрирована написанием простой игры, где ввод пользователя применяется для создания рассказа. Код EchoGame представлен ниже:

class EchoGame {

static void Main(string[] args) {

System.Console.WriteLine("Once upon a time in a far away" + "?");

string userInput1 = System.Console.ReadLine();

System.Console.WriteLine("a young prince ?");

string userInput2 = System.Console.ReadLine();

System.Console.WriteLine("One day while?");

string userInput3 ? System.Console.ReadLine();

System.Console.WriteLine("He came across a ?");

string userInput4 = System.ConsoleReadLine();

System.Console.WriteLine("The prince ?");

String userInput5 = System.Console.ReadLine();

System.Console.WriteLine("Once upon a time in a far away"

+ " {0}, a young prince {1}. \n One day"

+ "while {2}, He came across a (3). \n The "

+ "prince {4} ! ", userInput1, userInput2,

userInput3, userInput4, userInput5);

}

}

Точки вставки заменяются предоставленными аргументами, начиная с индекса {0}, который соответствует самой левой переменной (в данном случае userInput1). Можно подставлять не только строковые переменные, и не обязательно использовать только переменные или переменные одного типа. Любой тип данных, который может вывести метод WriteLine, допустим в качестве аргумента, включая строковые литералы или реальные значения. Не существует также ограничений на число точек вставки, которые могут добавляться к строке, пока их не больше общего числа аргументов. Отметим, что исключение точек вставки из строки приведет к тому, что переменные не будут выводиться. Необходимо, однако, иметь аргумент для каждой определенной точки вставки, индекс которой в списке аргументов соответствует индексу точки вставки. В следующем листинге, например, удаление {1} допустимо, пока имеется три аргумента. В этом случае {0} соответствует strA и {2} соответствует strC:

Console.WriteLine("hello {0} {1} {2}", strA, strB, strC);

Компиляция

При описании некоторых различий между JRE Java и CLR C# кратко упоминались некоторые детали того, как написанный на соответствующем языке код компилируется и выполняется. Хотя код обоих языков компилируется в некоторую промежуточную форму, байт-код версии Java никогда не компилируется повторно в собственные инструкции машины (если только не используется компилятор в собственный код). Вместо этого байт-код требует для выполнения среду времени выполнения, и в частности, виртуальную машину. Имя компилированного файла связано с именем файла, в котором находится исходный код, который в свою очередь связан с именем открытого класса в этом файле. В случае определения нескольких классов в одном файле каждое определение класса будет создавать файл класса, который соответствует имени определенного класса. Например, возьмем исходный файл Test.java со следующим кодом:

// Test.java

class x {}

class у {}

class z {}

Компиляция этого файла будет создавать три файла классов: х.class, у.class, z.class. Один (и только один для каждого исходного файла) из этих классов может быть объявлен открытым следующим образом:

// Test.java

public class x {}

class у {}

class z {}

В приведенном выше примере имя исходного файла Test.java должно быть изменено, чтобы соответствовать имени находящегося в нем открытого класса. Test.java поэтому станет х.java, чтобы код компилировался.

В противоположность этому, компилированный в IL код C# выполняется VES (Virtual Execution System), которая предоставляет поддержку для IL, загружая управляемый код, и JITters (которые преобразуют управляемый код в форме промежуточного языка в собственный код машины). Имя файла Hello.cs не связано с именем конечного исполняемого файла и может изменяться во время компиляции с помощью ключа /out. Если имя файла вывода не определено, то exe получит имя того файла исходного кода, который содержит метод main, a DLL получит имя первого указанного файла исходного кода. Фактически имя файла даже не связано с определениями любых классов, находящихся в файле. Класс Hello может быть определен в файле Goodbу.cs, который компилируется в несвязанный MisterHanky.exe.

SDK .NET поставляется вместе с компилятором, поэтому не нужно беспокоиться о получении специальных компиляторов для этого приложения. Откройте просто командную строку, перейдите в каталог, где сохранен файл hello.cs, и введите:

csc hello.cs

Файл будет компилирован в hello.exe. Хотя большинство программистов Java знакомы с этой формой низкоуровневой компиляции, важно заметить, что Visual Studio.NET предоставляет аналогичную функциональность, интегрированную в IDE. Например, изменение имени исполнимого файла можно легко выполнить, добавляя свойство имени сборки страницы свойств проекта. Чтобы сделать это производим щелчок правой кнопкой мыши на имени проекта в панели Solution Explorer и выбираем Properties или указываем Project в меню, в то время, когда имя проекта выделено, и выбираем пункт меню Properties. В папке Common Properties можно выбрать страницу General Properties и изменить свойство Assembly Name. Можно будет увидеть, что свойство для чтения Output File изменяется, отражая новое имя сборки.

Типы компиляции

Все файлы Java компилируются в файл байт-кода с расширением .class, который может выполниться виртуальной машиной. Внутри первого исходного кода необходимо предоставить соответствующую. Функциональность для создания того или иного типа приложения. Например, определенный ниже код будет создавать окно, аналогичное Form в Windows. Второй исходный файл AddLib.java является вспомогательным классом, используемым для выполнения сложения двух целых чисел. Можно заметить, что они включены в отдельные пакеты и JavaFrame импортирует класс AddLib. Пакеты и их эквивалент C# будут рассмотрены в следующем разделе:

// код для JavaFrame.java

Package com.javaapp;

import java.awt.*;

import java.io.*;

import com.javalib.AddLib;

public class JavaFrame extends java.awt.Frame {

public static void main (String[] args) {

JavaFrame jfrm = new JavaFrame();

jfrm.setSize(100, 100);

jfrm.setVisible(true);

AddLib lib = new AddLib();

jfrm setTitle("Frame Version " + lib.operationAdd(12, 23));

}

}

// код для AddLib.java

Package com.javalib;

public class AddLib {

public AddLib() {

}

public int operationAdd(int a, int b) {

return a + b;

}

}

Java предоставляет двухшаговый процесс для создания исполнимого файла или библиотечных единиц компиляции: откомпилировать файл(ы) и сделать файл доступным, предоставляя компилятору путь доступа к папке, где находятся файлы (можно написать пакетный файл, который будет делать это за один шаг). Класс можно сделать доступным с помощью ключа компилятора -classpath для определения, где компилятор может искать разрешение символов, не определенных в исходном коде (различные компиляторы могут иметь различные имена для этого ключа). Система имеет также переменную среды окружения Classpath. Если ключ -classpath не определен, то компилятор будет искать переменную окружения, если ключ -classpath определен, то он переопределяет любые записи Classpath, которые могут существовать среди переменных окружения для этой специфической компиляции.

Классы могут также объединяться в файл JAR, который необходимо сделать доступным таким же образом, как и папку, содержащую в себе класс. Классы внутри или вне файла JAR могут быть или не быть частью нуля или большего числа пакетов. В приведенном выше примере JavaFrame и AddLib необходимо будет откомпилировать в файлы классов. Путь доступа к этим файлам классов можно затем добавить к переменной окружения CLASSPATH. Когда путь доступа к классам задан, любые классы из пакета могут выполняться из любого каталога в системе, передавая полностью квалифицированное имя класса в виртуальную машину. Вызов с помощью SUN Microsystems JDK 1.3:

java javalib.JavaFrame

приводит к выполнению программы JavaFrame и созданию Frame с панелью заголовка, выводящей Java Frame Version 35.

Код в C# всегда после компиляции автоматически объединяется в тот или иной тип компонента. Единицы компиляции могут содержать столько файлов и определений классов, сколько потребуется. Способ использования этой функциональности снова делится между использованием командной строки и использованием IDE (в частности, Visual Studio.NET). Создание нового проекта с помощью VS.NET требует, чтобы был определен тип создаваемого проекта. Среди других имеются возможности создать консольное приложение, оконное приложение и библиотеку классов. Можно создать даже пустой проект и определить тип вывода позже. Процесс определения типа вывода проекта с помощью VS.NET описан дальше в этом разделе. В командной строке для изменения типа вывода используется ключ /target: <target-type>, где target-type является одной из строк:

□ Ехе

□ Library

□ Winexe

Можно добавить любое число файлов как аргументы, разделенные пробелами:

csc /target:<target-type> <file1> <file2> ... <filen>

Добавление нескольких файлов в единицу компиляции с помощью VS.NET является вопросом добавления отдельных файлов в проект. Можно легко изменить тип выходного файла, поменяв свойство Output Type на странице General Properties (смотрите выше детали доступа к странице с общими свойствами).

Пространства имен

Цель данного изложения состоит в создании версии C# файлов исходного кода JavaFrame и AddLib и рассмотрении деталей процесса создания кода C#. Так как два эти класса используют пакеты и импортирование, необходимо обсудить их эквиваленты в C#.

Классы Java могут располагаться в логических подразделениях, называемых пакетами. Пакет определяется как сущность, которая группирует классы вместе. Пакеты могут облегчить импорт другого кода программиста и более важно определить ограничения доступа к переменным и методам.

Пространства имен в C# предоставляют аналогичный механизм для объединения управляемых классов, но являются значительно более мощными и гибкими. Здесь речь идет об управляемых классах, а не специальных классах C#, так как классы в пространстве имен могут быть из любого, соответствующего CLS, языка (вспомните, что CLS является независимым от языка). Пакеты и пространства имен, однако, существенно различаются своей реализацией. Класс Java, который необходимо, например, сделать частью пакета com.samples, должен иметь в качестве первой строки кода в файле Package com.samples. Это, конечно, исключает какие-либо комментарии. Любой код внутри этого файла автоматически становится частью указанного пакета. Имя пакета в Java также связывается с папкой, содержащей файл класса в том смысле, что они должны иметь одинаковые имена. Пакет com.samples поэтому должен находиться в файле, который существует в папке com\samples. Давайте рассмотрим некоторые примеры того, как работают пакеты.

// package_samples.java

package samples.on; // отображает прямо в папку, где находится файл класса

public class Packaging {

int x;

public class Internal {

// находится автоматически в том же пакете

}

public static void main(String args[]) {

}

}

class Internal {

// находится автоматически в том же пакете

}

Примеры того, как этот код может выполняться, приведены ниже. Это, конечно, предполагает, что файл класса был сделан доступным для JRE:

□ Из командной строки:

java samples.on.Packaging

□ Как непосредственная ссылка в коде:

// Referencer.java

public class Referencer {

samples.on.Packaging pack = new samples.on.two.three.Packaging();

□ Используя директиву import, можно опустить полностью квалифицированные имена пакетов, поэтому Referencer запишется как:

// Referencer.java

import samples.on.*;

public class Referencer{

Packaging pack = new Packaging();

}

Помещение класса в пространство имен достигается в C# с помощью ключевого слова namespace с идентификатором и заключением целевого класса в скобки. Вот пример:

// namespace_samples.cs

namespace Samples.On {

using System;

public class Example {

public Example() {

}

}

}

Преимущество использования скобок для явного ограничения пространства имен состоит в том, что это задает определенный пользователем тип в реальном классе, определенном в файле, а не в самом файле. В Java файлы и папки косвенно представляют структуры языка, так как они аналогичны классам и пакетам, содержащим эти классы. В C# файлы не связаны принудительно с чем-либо, поэтому они становятся местом, где располагается определение класса, а не частью какой-либо структуры языка. Пространства имен также не связаны с папками. Следовательно, в одном файле можно ввести несколько пространств имен без всяких ограничений. Можно, например, добавить определение нового класса и поместить его в новое пространство имен в том же файле и по-прежнему оставаться в границах языка:

// namespace_samples.cs

namespace Samples.On {

using System;

public class Example {

public Example() {

}

}

}

namespace Com.Cslib {

using System;

using System.Collections;

public class AddLib {

public AddLib() {

}

public int operationAdd(int a, int b) {

return a + b;

}

}

}

Пространства имен вводятся с помощью директивы using <namespace name>, где <namespace name> является именем пространства имен. В C# не требуется использовать *, так как директива using неявно импортирует все элементы указанного пространства имен. Другим преимуществом является то, что пространства имен могут быть добавлены исключительно в конкретный класс. Хотя классы Example и AddLib выше определены в файле namespace_samples.cs. Example не имеет доступа к пространству имен System.Collections, несмотря на то, что AddLib его имеет. Однако инструкция import из Java не является специфической для класса. Она импортирует указанные элементы в файл. Вновь обратимся к х.java.

// х.java

public class x {

}

class у {

}

class z {

}

Если добавить инструкцию импорта, такую как import java.util.Hashtable, все классы, определенные внутри этого файла, будут иметь доступ к классу Hashtable. Код ниже будет компилироваться:

// x.java

package samples;

import java.util.Hashtable;

public class x {

Hashtable hash = new Hashtable();

}

class у {

Hashtable hash = new Hashtable();

}

class z {

Hashtable hash = new Hashtable();

}

Пространства имен можно также определять внутри другого пространства имен. Этот тип гибкости недоступен в Java без создания подкаталогов. Приведенное выше пространство Com.Cslib можно расширить следующим образом:

namespace Com.Cslib {

using System;

public class AddLib {

public AddLib() {

}

public int operationAdd(int a, int b) {

return a + b;

}

}

namespace Ext {

public class AddLib {

public AddLib() {

}

public int operationAdd(int a, int b) {

return a + b;

}

}

}

}

Пакет Java com.javalib можно расширить, чтобы отобразить приведенный выше код, создав новую папку \EXT в каталоге com\javalib. В этой папке создается файл исходного кода AddLib.java следующим образом:

package com.javalib.ext;

public class AddLib {

public AddLib() {

}

public int operationAdd(int a, int b) {

return a + b;

}

}

Отметим, что имя пакета было расширено для этого класса до com.javalib.ext.

Внутреннее пространство имен и подпакеты доступны с помощью оператора точки "."; следовательно, можно было в C# извлечь расширенный AddLib с помощью нотации Com.Cslib.Ext.AddLib. В Java можно было бы использовать com.javalib.ext.AddLib.

Приведенный выше пример показывает одно сходство между пакетами Java и пространствами имен C#. Даже если они не используются для внешнего представления, пространства имен, так же как и пакеты, предоставляют прекрасный способ создания глобально уникальных типов, свою собственную песочницу в мире сборок независимых поставщиков. В той степени насколько это имеет отношение к C#, Com.Cslib.AddLib является не тем же классом, что и Com.Cslib.Ext.AddLib.

Классы Java являются частью пакета, нравится им это или нет. Все классы, созданные без указания пакета, предполагают включение в пакет по умолчанию. C# имитирует эту функциональность. Даже если не объявить пространство имен, оно будет создано по умолчанию. Оно присутствует в каждом файле и доступно для использования в именованных пространствах имен. Так же как в Java нельзя изменить информацию о пакете, пространства имен нельзя модифицировать. Пакеты могут охватывать несколько файлов в одной папке, пространство имен может охватывать несколько файлов в любом числе папок и даже в нескольких сборках (сборки будут рассмотрены в следующем разделе). Два класса, охватываемые пространством имен А, которые определены в отдельных файлах и существуют в отдельных папках, оба являются частью пространства имен А.

Чтобы получить доступ к элементу в пространстве имен, необходимо либо использовать полностью квалифицированное имя типа (в приведенном выше примере это Com.Cslib.AddLib) или импортировать элемент пространства имен в текущее пространство имен, используя директиву using. Отметим, что по умолчанию доступность типов данных внутри пространства имен является внутренней. Необходимо явно отметить типы данных как открытые (public), если требуется сделать их доступными без полной квалификации, но придерживаться такой стратегии строго не рекомендуется. Никакие другие модификаторы доступа не разрешены. В Java внутренние типы пакета могут также помечаться как final или abstract, или не помечаться вообще (этот доступ по умолчанию открывает их только для потребителей внутри пакета). Модификаторы доступа будут рассматриваться позже в этом приложении.

Последним атрибутом, который относится к пространству имен, но не имеет отношения к пакетам, является возможность задания алиаса для using. Алиасы using существенно облегчают квалификацию идентификатора для пространства имен или класса. Синтаксис очень простой. Предположим, что имеется пространство имен Very.Very.Long.Namespace.Name. Можно определить и использовать алиас using для пространства имен следующим образом:

using WLNN = Very.Very.Long.Namespace.Name;

Конечно, имя псевдонима (алиаса) является произвольным, но должно следовать правилам именования переменных в C#.

Создание и добавление библиотек при компиляции

Ранее, при обсуждении компиляции и единиц компиляции кратко было сказано о концепции библиотек. Если создана библиотека, то необходимо, чтобы она была доступна для всех потенциальных потребителей. В Java это делается добавлением пути доступа к папке, содержащей классы библиотеки, в переменную окружения Classpath. Конечно, чтобы упростить это, можно добавить файлы класса в папке в JAR и поместить путь доступа к файлу jar в Classpath. В любом случае работа загрузчика классов состоит в нахождении всех неразрешенных ссылок, и он будет искать их в Classpath.

C# предоставляет совсем другие механизмы для упаковки классов в библиотеку. По умолчанию все файлы C# в проекте станут частью единицы компиляции при использовании VS.NET. Если применяется командная строка, необходимо будет явно добавлять каждый файл, который должен быть частью единицы компиляции, как описано выше.

Библиотеки кодов компилируются в РЕ (portable executable — переносимый исполнимый) тип файла. Необходимо в этом месте сделать различие между библиотеками кодов, о которых идет речь, и проектом Class Library в C#, который может создаваться с помощью IDE VS.NET. Под библиотекой кода понимается повторно используемое множество файлов C#, соединенных вместе в некоторой единице компиляции, поэтому происходит ссылка на файл РЕ, а не на реальную DLL или ЕХЕ. Библиотека кода такого вида более часто называется сборкой, поэтому это название будем использовать в дальнейшем, чтобы избежать путаницы.

Так же как файл JAR, сборка имеет манифест, который описывает ее содержимое. Манифест сборки C# делает, однако, значительно больше. Он содержит все метаданные (совокупность данных, описывающая, как связаны элементы сборки), необходимые для определения требований версии, идентичности безопасности и всей информации, служащей для определения области действия сборки и разрешения ссылок на ресурсы и классы. Манифест сборки может храниться либо в файле РЕ (ЕХЕ или DLL) с кодом IL, либо как автономный файл, который содержит только данные манифеста сборки. В .NET пространства имен, содержащиеся внутри сборки, представлены внешнему миру (то есть, любым потребителям сборки, таким, как другие сборки) с помощью информации метаданных о типах, хранящейся в манифесте сборки.

Классы в namespace_samples.cs могут компилироваться в библиотеку с помощью команды:

CSC /target:library /out:FirstLibrary.dll namespace_samples.cs

Чтобы сделать информацию о типах из одной сборки доступной для других сборок, можно использовать два ключа компилятора: /addmodule и /reference. Они являются по сути одинаковыми, за исключением того, что /reference предназначен для сборок с манифестом сборки, в то время как /addmodule предназначен для сборок без манифеста сборки (модуль не имеет манифеста сборки). Синтаксис для добавления внешних ссылок из командной строки будет следующим:

csc /reference: <lib.dll>; <libn.cs> <filename.exe>

или:

csc /addmodule: <lib.dll>; <libn.cs> <filename.exe>

Чтобы добиться этого с помощью VS.NET, сделайте щелчок правой кнопкой мыши на папке References своего проекта в Solution Explorer и выберите Add Reference. Появится диалоговое окно, содержащее ряд доступных ссылок и позволяющее просматривать файловую систему в поисках ссылок. Можно также вызвать это диалоговое окно, выбирая пункт Add Reference из меню Project, когда выделен требуемый проект. Добавление ссылки в текущую сборку обычно копирует упомянутый файл в папку проекта, где будет располагаться новая сборка.

Давайте создадим теперь потребителя для библиотеки AddLib, который аналогичен по форме и функциональности созданному ранее классу JavaFrame:

// LibConsumer.cs

namespace Com.CSapp {

using System;

using System.Windows.Forms;

using Com.CsLib;

public class Form1 : System.Windows.Forms.Form {

public Form1() {

AddLib al = new AddLib();

this.Text = "C# Form version " + al.operationAdd(12, 23);

}

public override void Dispose() {

base.Dispose();

}

Static void Main(string[] args) {

Form1 f1 = new Form1();

Application.Run(f1);

}

}

}

Этот код можно компилировать из командной строки со ссылкой на FirstLibrary.dll, введя следующую команду:

csc /reference: FirstLibrary.dll /target:exe /out:EmptyForm.exe LibConsumer.cs

Используя VS.NET, надо сначала сослаться на FirstLibrary.dll, как описано ранее, и затем собрать приложение. Когда приложение будет успешно собрано, выполните его. Оно должно создать форму Windows с заголовком C# form Version 35.

Обнаружение и разрешение

Мы уже обсудили, как JRE разрешает ссылки на другие классы, используя загрузчик классов для проверки во время выполнения переменной окружения Classpath. CLR проходит также ряд шагов, часто называемых зондированием (probing), при попытке найти сборку и разрешить ссылку на сборку. Попытка выполнить EmptyForm.exe приводит в движение ряд вещей. По умолчанию весь управляемый код загружается в домен приложения и обрабатывается определенным потоком выполнения операционной системы. Указанные сборки, означающие, что их типы используются в коде домена приложения, также должны загружаться, прежде чем их можно будет выполнить. CLR проходит несколько стадий, чтобы обнаружить и связаться с указанной сборкой.

Так как поведение по соединению со сборками может конфигурироваться на основе конфигурационных файла приложения, файла издателя и файла машины/администратора, CLR должна брать эту конфигурационную информацию, чтобы обеспечить извлечение соответствующей версии указанной сборки. Все эти файлы основаны на XML и следуют похожему синтаксису. Они предоставляют такую информацию, как перенаправление связывания, расположение кода и режимы связывания для определенных сборок. Обычно правильная версия сборки определяется некоторой комбинацией трех конфигурационных файлов и собственным манифестом сборки.

CLR сначала проверяет, не переопределяет ли информация конфигурационного файла приложения информацию, которая хранится в манифесте вызывающей сборки. Затем CLR проверяет конфигурационный файл издателя. Этот файл присутствует, только когда приложение было обновлено новыми версиями одного или нескольких компонентов приложения. Это прежде всего используется для переопределения информации в конфигурационном файле приложения, чтобы приложение выбрало новую версию компонента. CLR затем проверяет конфигурационный файл машины/администратора. Хотя он просматривается последним, настройки, присутствующие в этом файле, получают приоритет по отношению ко всем другим конфигурационным настройкам. По сути администраторы используют admin.cfg, чтобы определить ограничения связывания, локальные для данной машины.

Затем CLR необходимо найти сборку. В Java JRE будет действовать в текущем каталоге и в classpass, чтобы найти класс, необходимый для разрешения ссылки. В определении расположения заданной сборки CLR полагается на элемент <codeBase>, связанный с упомянутыми выше конфигурационными файлами. Если такой элемент не предоставлен, то CLR ищет файл (называемый probing) в корневом каталоге приложения, во всех каталогах, перечисленных в конфигурационных файлах элемента <probing> и во всех подкаталогах корня приложения, которые имеют такое же имя, как и зондируемая сборка. Эти элементы всегда определяются относительно корневого каталога приложения. Отметим, что CLR всегда ищет имя сборки, соединенное с двумя допустимыми расширениями файлов РЕ — .exe и .dll.

Наконец, если ссылка сделана на сборку с устойчивым именем, CLR будет искать в глобальном кэше сборок. Устойчивые имена и глобальный кэш сборок мы рассмотрим более подробно в следующем разделе.

Строгие имена и глобальный кэш

Устойчивое (строгое) имя делает сборку глобально уникальной. Оно состоит из идентификатора сборки, открытого ключа и цифровой подписи. Так как сборка, созданная с одним открытым ключом имеет другое имя, чем сборка, созданная с другим открытым ключом, то уникальность имени сборки гарантирована. Нельзя изменить сборку с устойчивым именем, не имея доступа к открытому ключу, использованному при ее создании, следовательно, устойчивое имя гарантирует, что никто не сможет создать другие версии этой сборки. Устойчивые имена предоставляют также жесткий контроль целостности. Создание устойчивого имени является двухшаговым процессом. Сначала необходимо создать или иметь ключевую пару. Создание ключевой пары можно выполнить в командной строке с помощью команды:

sn -k <имя файла ключа>

Например, чтобы создать файл ключа examplekey.key, надо ввести:

sn -k examplekey.key

После создания файл ключа используется для присвоения устойчивого имени сборке двумя способами. Из командной строки можно использовать утилиту alink следующим образом:

al /keyfile: <имя файла ключа> <имя сборки>

Чтобы задать для FirstLibrary.dll строгое имя, можно выполнить:

al /keyfile:examplekey.key FiretLibrary.dll

или в VS.NET изменить атрибут [assembly: AssemblyKeyFile("")] в файле AssemblyInfo.cs. Замените просто пустую строку на строку, представляющую имя файла ключа. Сборку FirstLibrary.dll можно затем ассоциировать с ключом, меняя атрибут AssemblyKeyFile на [assembly: AssemblyKeyFile("examplekey.key")]. Какой бы метод ни использовался, конечный результат получается тот же самый. CLR будет устанавливать ключ в файл с помощью Crypto Service Provider (CSP). Работа CSP не представлена в данном приложении.

Все сборки в глобальном кэше сборок должны иметь устойчивое имя. Глобальный кэш сборок применяется для хранения сборок, специально созданных для общего использования несколькими приложениями на машине. Существует несколько способов размещения сборки в глобальном кэше сборок. Можно использовать программу установки, созданную для работы с глобальным кэшем сборок, .NET Framework SDK предоставляет программу установки с именем gacutil.exe. Чтобы поместить FirstLibrary.dll в глобальный кэш сборок, воспользуйтесь командой:

gacutil -i FirstLibrary.dll

Можно использовать Проводник Windows для перетаскивания сборок в кэш. Отметим, что необходимо иметь привилегии администратора на машине, чтобы устанавливать сборки в глобальный кэш сборок, независимо от используемого подхода.

Типы данных

Типы данных в Java и в C# можно сгруппировать в две основные категории: типы данных значений и ссылочные типы. Существует только одна категория типа данных значений в Java. Все типы данных значений являются по умолчанию примитивными типами данных языка. C# предлагает более обильный ассортимент. Типы данных значений можно разбить на три основные категории:

□ Простые типы

□ Типы перечислений

□ Структуры

Давайте рассмотрим каждый из них по очереди.

Простые типы

Ранее в разделе о ключевых словах было сделано подробное сравнение между примитивными типами данных Java и их эквивалентами в C# (по размеру). Был также введен ряд типов данных значений, представленных в C#, которых Java не имеет. Это были 8-битовый без знака byte (отличный от byte в Java, который имеет знак и отображается в sbyte в C#), короткое целое без знака ushort, целое без знака uint, длинное целое без знака ulong и, наконец, высокоточное decimal.

Целые значения

Когда целое число не имеет суффикса, то тип, с которым может быть связано его значение, оценивается в порядке int, uint, long, ulong, decimal. Целые значения представляются как десятичные или шестнадцатеричные литералы. В коде ниже результат равен 52 для обоих значений:

int dec = 52;

int hex = 0x34;

Console.WriteLine("decimal {0}, hexadecimal {1}", dec, hex);

Символьные значения

char представляет одиночный символ Unicode длиной два байта. C# расширяет гибкость присваивания символов, допуская присваивание с помощью шестнадцатеричной кодированной последовательности с префиксом \х и представление Unicode с помощью \u. Также нельзя неявно преобразовать символы в целые числа. Все другие обычные кодированные последовательности языка Java полностью поддерживаются.

Логические значения

bool, boolean в Java, используются для представления значений true и false непосредственно или как результат равенства, как показано ниже:

bool first_time = true;

cool second_time = (counter < 0);

Значения decimal

C# вводит тип данных decimal, который является 128-битовым типом данных, представляющим значения в диапазоне от примерно 1.0×10^28 до 7.9×10^28. Они предназначены прежде всего для финансовых и денежных вычислений, где точность является предельно важной. При присваивании типу decimal значения, к литеральному значению должно добавляться m, иначе компилятор считает значение типом double. Так как decimal не может неявно преобразовываться в double, то отсутствие m требует явного преобразования  типа:

decimal precise = 1.234m;

decimal precise = (decimal)1.234;

Значения с плавающей точкой

Значения с плавающей точкой могут быть либо double, либо float. При вычислениях все другие простые типы значений будут неявно преобразовываться, в соответствующий тип с плавающей точкой, если присутствует тип с плавающей точкой. Действительный числовой литерал с правой стороны оператора присваивания интерпретируется как double по умолчанию. Так как не существует неявного преобразования из float в double, может оказаться удивительным возникновение ошибок компиляции. Пример ниже иллюстрирует эту проблему:

float f = 5.6;

Console.WriteLine(x);

Этот пример будет создавать сообщение об ошибке компиляции, показанное ниже.

C:\_wrox\c# for java developers\code\SuperEX\Class1.cs(15): Literal of type double cannot De implicitly converted to type 'float'; use an 'F' suffix to create a literal of this type

Существует два способа решения этой проблемы. Можно преобразовать литерал во float, но сам компилятор предлагает более разумную альтернативу. Использование суффикса F говорит компилятору, что это литерал типа float, а не double. Хотя и не обязательно, но можно использовать суффикс D, чтобы указать на литерал типа Double.

Типы перечислений

Перечисление является отдельным типом, состоящим из множества именованных констант. В Java можно добиться этого, используя переменные static final. В этом смысле перечислении могут в действительности быть частью класса, который их использует. Другой подход состоит в определении перечисления как интерфейса. Пример ниже иллюстрирует такую концепцию:

interface Color {

static int RED = 0;

static int GREEN = 1;

static int BLUE = 2;

}

Этот подход проблематичен тем, что он не является безопасным в отношении типов данных. Любое считанное или вычисленное целое используется в качестве цвета. Можно, однако, программным путем реализовать перечисление с безопасными типами в Java, используя вариант шаблона (паттерна) Singleton, который ограничивает класс предопределенным числом экземпляров. Приведенный далее код показывает, как это можно сделать:

final class Day {

// final, поэтому нельзя создавать подклассы этого класса

private String internal;

private Day(String Day) {internal = Day;} // закрытый конструктор

public static final Day MONDAY = new Day("MONDAY");

public static final Day TUESDAY = new Day("TUESDAY");

public static final Day WEDNESDAY = new Day("WEDNESDAY");

public static final Day THURSDAY = new Day("THURSDAY");

public static final Day FRIDAY = new Day("FRIDAY");

}

Как можно видеть из приведенного выше примера, перечисленные константы связаны не с примитивными типами, а с объектными ссылками. Также, поскольку класс определен как final, то нельзя создать его подклассы, следовательно, никакие другие классы не могут из него создаваться. Конструктор отмечен как закрытый, поэтому другие методы не могут использовать класс для образования новых объектов. Единственные объекты, которые будут созданы для этого класса, это статические объекты, которые класс формирует для себя при первой ссылке на класс.

Можно согласиться с тем, что хотя концепция достаточно простая, обход включает развитую технику и она может не сразу стать понятной новичку, в конце концов нам требуется только список констант; C#, в противоположность, предоставляет встроенную поддержку перечислении, которая обеспечивает также безопасность типов. Чтобы объявить в C# перечисление, используется ключевое слово enum. В своей простейшей форме enum может выглядеть как следующий код:

public enum Status {

Working,

Complete,

BeforeBegin

}

В приведенном выше случае первое значение равно 0 и enum увеличивает значения. Complete будет 1 и т.д. Если по какой-то причине требуется, чтобы enum представлял другие значения, можно сделать это, присваивая такие значения следующим образом:

public enum Status {

Working = 131,

Complete = 129,

BeforeBegin = 132

}

Имеется также возможность использовать другие числовые целые типы 'наследуя' от long, short или byte. int всегда является типом по умолчанию. Эта концепция проиллюстрирована ниже:

public enum Status : int {

Working,

Complete,

BeforeBegin

}

public enum SmallStatus : byte {

Working,

Complete,

BeforeBegin

}

public enum BigStatus : long {

Working,

Complete,

BeforeBegin

}

Обратите внимание, что существует большое различие между этими тремя перечислениями, связанное напрямую с размером типа данных, от которого они наследуют. byte в C#, например, может содержать 1 байт памяти. Это означает, что SmallStatus не может с одержать более 255 констант или задать значение любой своей константы больше 255. Следующий листинг показывает, как можно использовать оператор sizeof() для идентификации различий между версиями Status:

int х = sizeof(Status);

int у = sizeof(SmallStatus);

int Z = sizeof(BigStatus);

Console.WriteLine("Regular size:\t{0}\nSmall size \t{1}\nLarge size:\t{2}", x, y, z);

После компиляции листинг создаст результаты, показанные ниже:

Regular size: 4

Small size:   1

Large size:   8

Структуры

Одним из основных различий между структурой C# (идентифицируемой ключевым словом struct) и классом является то, что то умолчанию struct передается посредством значения, в то время как объект передается по ссылке. Как хорошо известно, объекты создаются в куче, в то время как переменные, которые на них ссылаются, хранятся в стеке. Структуры, со своей стороны, создаются и хранятся в стеке. Их аналога в Java не существует. Структуры имеют конструкторы и методы, у них могут быть индексаторы, свойства, операторы и даже вложенные типы. С помощью struсt создаются типы данных, которые ведут себя таким же образом, как встроенные типы. Ниже приведен пример использования структур:

public struct WroxInt {

int internalVal;

private WroxInt(int x) {

internalVal = x;

}

public override string ToString() {

return Int.ToString(internalVal);

}

public static impicit operator WroxInt(int x) {

return new WroxInt(x);

}

}

public static void UseWroxInt() {

WroxInt wi = 90;

Console.WriteLine(wi);

}

Этот пример показывает типы, которыми владеют мощные структуры. WroxInt используется почти так же, как и встроенный тип int. Как известно, не существует способа сделать что-нибудь подобное в Java. Ряд других достоинств и ограничений, связанных с использованием структур, представлен ниже:

□ struct нельзя наследовать от другой struct или от класса.

□ struct не является базой для класса

□ Хотя struct может oбъявлять конструкторы, эти конструкторы должны получать не меньше одного аргумента.

□ Члены struct не могут иметь инициализаторов.

□ Возможно создание экземпляра struct без использования ключевого слова new.

□ struct может реализовывать интерфейсы.

Атрибуты используются со структурами чтобы добавить им дополнительную мощь и гибкость. Атрибут StructLayout в пространстве имен System.Runtime.InteropServices, например, применяется для определения компоновки полей в struct. Это свойство подходит и для создания структуры, аналогичной по функциональности union в С/C++, union является типом данных, члены которого находятся в одном блоке памяти. Он может использоваться для хранения значений различных типов в одном блоке памяти. union годится и в том случае, когда неизвестно, каким будет тип полученных значений. Конечно, никакого рeaльного преобразования не происходит, фактически не существует никакие базовых проверок допустимости данных. Один и тот же набор битов интерпретируется различным образом. Рассмотрим пример того, как union создается с помощью struct:

[StructLayout(LayoutKind.Explicit)]

public struct Variant {

[FieldOffset(0)] public int intVal;

[FieldOffset(0)] public string stringVal;

[FieldOffset(0)] public decimal decVal;

[FieldOffset(0)] public float floatVal;

[FieldOffset(0)] public char charVal;

}

Атрибут FieldOffset, применяемый к полям, используется для задания физического расположения указанного поля. Задание начальной точки каждого поля как 0 гарантирует, что любое сохранение данных в одном поле перезапишет практически любые данные, которые там находятся. Отсюда следует, что общий размер полей равен размеру наибольшего поля, в данном случае decimal.

Ссылочные типы

Ссылочные типы хранят ссылку на данные, которые существуют в куче. Только адреса памяти хранимых объектов сохраняются в стеке. Тип объекта, массивы, интерфейсы тип класса и делегаты являются ссылочными типами. Объекты, классы и отношения между ними не отличаются в Java и C#. Интерфейсы и их использование также похожи в обоих языках. Одно из основных различий, которое, вероятно, уже встречалось, состоит в том, что C# не имеет ключевых слов extends и implements. Оператор двоеточия (:) заменяет оба ключевых слова Java, и, как было показано ранее, директива using аналогична инструкции Java import. Строки тоже используются одинаково в C# и Java. C# вводит также новый тип ссылочного типа называемого делегатом. Делегаты представляют безопасную, с точки зрения типов, версию указателей функций. Они будут рассмотрены позже в этой главе.

Массивы

C# поддерживает "неровные" массивы и добавляет многомерные массивы. Может сбить с толку то, что Java не делает между ними различий:

int [] х = new int[20]; // как в Java, только [] должны следовать

// за типом

int [,] у = new int[12, 3]; // то же самое, что int у[] [] = new

// int[12][3];

int[][] z = new int[5][]; // то же самое, что и int x[][] = new

// int [5][];

Примечание. Ключевое слово int[] обозначает реальный тип данных, поэтому синтаксически оно записывается таким образом. Нельзя, как в Java, поместить двойные скобки перед или после переменной. Прежде чем перейти к дополнительным деталям о ссылочных типах и обсуждению таких концепции, как классы, давайте поговорим немного об операциях. Следующий раздел посвящен операторам.

Операторы

В Java конечный результат применения оператора к одному или нескольким операндам является новым значением для одного или нескольких вовлеченных операндов. C# предоставляет аналогичную функциональность, однако, как можно будет увидеть в разделах ниже, существуют незначительные различия между C# и Java даже в этой области. В этом разделе мы охватим разные группы операторов в C# и обсудим чем отличается в C# и Java каждая группа.

Присваивание

C# и Java используют знак = для присваивания значений переменным. В C#, как и в Java, переменные присвоенные объектам содержат только ссылку или "адрес" на этот объект, а не сам объект. Присваивание одной ссылочной переменной другой таким образом просто копирует "адрес" в новую переменную. Следовательно обе переменные теперь имеют возможность делать ссылку на один объект. Эту концепцию легко проиллюстрировать с помощью примера. Рассмотрим класс ExOperators, приведенный ниже:

public class EXOperators {

internal int р;

public EXOperators() {}

public static void Main() {

ExOperators one = new EXOperators();

one.p = 200;

EXOperators two;

two = one;

two.p = 100;

Console.WriteLine(two.p);

Console.WriteLine(one.p);

}

}

Пока проигнорируем ключевое слово internal перед переменной р. Оно является модификатором доступа, который будет рассмотрен далее в этой главе. Достаточно сказать, что оно делает переменную р видимой для метода Main. Приведенный выше пример создает экземпляр объекта EXOperators и сохраняет его в локальной переменной one. Эта переменная затем присваивается другой переменной — two. После этого значение p в объекте, на который ссылается two, изменяется на 100. В конце выводится значение переменной p в обоих объектах. Компиляция и выполнение этого даст результат 100 дважды, указывая, что изменение two.р было тем же самым, что изменение значения one.р.

Сравнение

Операторы сравнения обычно совпадают по форме и функциональности в обоих языках. Четырьмя основными операторами являются < — меньше, чем, > — больше, чем, <= — меньше или равно и >= — больше или равно.

Чтобы определить, принадлежит ли объект заданному классу или любому из классов предков, Java использует оператор instanceof. Простой пример этого приведен в листинге ниже:

String у = "a string";

Object х = у;

if (х instanceof String) {

System.out.println("х is a string");

}

В C# эквивалентом instanceof является оператор is. Он возвращает true, если тип времени выполнения заданного класса совместим с указанным типом. Версия C# приведенного выше кода будет иметь следующую форму:

string у = "a string";

object х = у;

if (х is System.String) {

System.Console.WriteLine("x is a string");

}

Операторы равенства aрифметические, условные, побитовые, битового дополнения и сдвига

В обоих языках операторы равенства могут использоваться для тестирования чисел, символов, булевых примитивов, ссылочных переменных. Все другие операторы, упомянутые выше работают таким же образом.

Преобразование типов

Преобразование в Java состоит из неявного или явного сужения или расширения преобразования типа при использовании оператора (). Можно выполнить аналогичное преобразование типа в C#. C# также вводит ряд действенных способов, встроенных в язык, среди них мы выделим упаковку и распаковку.

Так как типы значений являются блоками памяти определенного размера, они прекрасно подходят для целей ускорения. Иногда, однако удобство объектов желательно иметь для типов значений. Упаковка и распаковка предоставляет механизм, который формирует линию соединения между типами значений и ссылочными типами, позволяя преобразовать их в и из объектного типа.

Упаковка объекта означает неявное преобразование любого типа значения в объектный тип. Экземпляр объекта создается и выделяется, а значение из типа значения копируется в новый объект. Здесь приведен пример, показывающий, как упаковка работает в C#:

// BoxEx.cs

public class OverflowEX {

public static void Main(String[] args) {

int x = 10;

Object obj = (Object)x;

Console.WriteLine(obj);

}

}

Такой тип функциональности недоступен в Java. Код, представленный ниже не будет компилироваться, так как примитивы не могут преобразовываться в ссылочные типы:

// BoxEx.java

public class BoxEX {

public static void main(String args[]) {

int x = 10;

object obj = (object)x;

System.out.println(obj);

}

}

Распаковка является просто преобразованием объектного типа, приводящим значение снова к соответствующему типу значения. Эта функциональность опять же недоступна в Java. Можно изменить предыдущий код для иллюстрации этой концепции. Сразу заметим, что в то время как упаковка является неявным преобразованием типа, распаковка требует явного преобразования типа. Вот новая реализация BoxEx.cs:

// BoxEX.cs

public class OverflowEX {

public static void Main(String[] args) {

int x = 10;

Object, obj = (Object)x;

Console.WriteLine(obj);

int у = (int)obj;

Console.WriteLine(y);

}

}

Другим эффективным способом C#, предназначенным для преобразования типов, является возможность определить специальные операторы преобразования. Определенные пользователем преобразования выполняются из типа данных в тип, а не из экземпляра в экземпляр, поэтому они должны быть статическими операциями. Можно использовать ключевое слово implicite для объявления определенных пользователем преобразований из одного типа в другой. Предположим, что имеются два класса Man и Car, которые полностью не связаны. Создадим определенное пользователем преобразование, которое переводит один класс в другой. Ниже приведен листинг Man.cs:

public class Man {

int arms, legs;

string name;

public Man(){}

public int Arms {

set {

arms = value;

}

get {

return arms;

}

}

public string Name {

set {

name = value;

}

get {

return name;

}

}

public int Legs {

set {

legs = value;

}

get {

return legs;

}

}

}

Как можно видеть из приведенного примера, класс Man имеет три свойства: можно задать или извлечь Legs, Arms, Name. Ниже представлен листинг класса Car:

public class Car {

int wheels, doors, headlights;

public Car(int wheels, int doors, int headlights) {

this.wheels = wheels;

this.doors = doors;

this.headlights = headlights;

}

}

He существует на самом деле определенных правил о том, что включать в реализацию специального преобразования. Необходимо, однако, сопоставлять как можно больше пар полей данных между двумя операндами. В случае данного примера поле Car.wheel будет сопоставлено с Man.legs, а поле Car.doors с Man.arms. Не существует поля в Car, которое представляет что-нибудь похожее на Man.Name, но это не мешает использовать его. Можно, скажем, сопоставить Car.headlights с длиной строки, которая хранится в Man.name. Любая реализация, которая имеет смысл для программиста, будет приемлема. В этом случае Man.name не сопоставляется с Car.headlights, вместо этого для headlights жестко кодируется 2, когда делается преобразование, и полностью отбрасывается Man.name. Следующий код содержит модификацию класса Car:

public class Car {

int wheels, doors, headlights;

public Car(int wheels, int doors, int headlights) {

this.wheels = wheels;

this.doors = doors;

this.headlight = headlights;

}

public static implicit operator Car(Man man) {

return new Car(man.Legs, man.Arms, 2);

}

public static explicit operator(Car car) {

Man man = new Man();

man.Arms = car.doors;

man.Legs = car.wheels;

man.Name = "john";

return man;

}

}

Мы добавим также переопределенные версии для методов ToString() обоих классов, чтобы вывести содержимое объекта Car. Это делается так:

// для Man.cs

public override string ToString() {

return "[arms:" + arms + "|legs:" + legs + "|name:" + name + "]";

}

// для Car.cs

public override string ToString() {

return "[wheels:" + wheels + "|doors:" + doors + "|headlights:" + headlights + "]";

}

Листинг кода ниже показывает использование специального преобразования:

// BoxEx.cs

public class OverflowEX {

public static void Main(String[] args) {

Car car = new Car (4, 5, 2);

Man man = (Man) car; // использует явное специальное преобразование

Console.WriteLine("Man - ");

Console.WriteLine(man);

Console.WriteLine();

Car car2 = man; // использует неявное специальное преобразование

Console.WriteLine("Car - ");

Console.WriteLine(car2);

}

}

Компиляция и выполнение этого кода создает показанные ниже результаты:

Man -

[arms:5|legs:4|name:john]

Car -

[wheels:4|doors:5|headlights:2]

Перезагрузка

В начале важно отметить, что перезагрузка операторов не определена в CLS. Однако CLS обращается к ней, потому что языки, обеспечивающие ее функциональность, делают это способом, который могут понять другие языки. Таким образом, языки, которые не поддерживают перезагрузку операторов, все-таки имеют доступ к базовой функциональности. Java является примером языка, который не поддерживает перезагрузку операторов, — ни одна из концепций, рассмотренных в этом разделе, не может ее использовать. Спецификация среды .NET включает ряд рекомендаций для проведения перезагрузки операторов.

□ Определите операторы на типах данных значений, которые логически являются встроенным типом языка (таким, как System.Decimal).

□ Используйте методы перезагрузки операторов, включающие только тот класс, на котором определены методы.

□ Применяйте соглашения об именах и сигнатурах, описанные в CLS.

□ Перезагрузка операторов полезна в случаях, где точно известно, каким будет результат операции.

□ Предоставьте альтернативные сигнатуры. Не все языки поддерживают вызов перезагруженных операторов. Поэтому постарайтесь всегда включать вторичный метод с подходящим специфическим для домена именем, который имеет эквивалентную функциональность.

Перезагрузка операторов связана как с определенными пользователем преобразованиями, так и с типом данных, а не с экземпляром. Это означает, что она связана со всем типом данных, а не с каким-то одним экземпляром объекта, то есть операция всегда должна быть static и public.

В нижеследующем примере создается тип данных значения Wheels, который может выполнять перезагруженное сложение с самим собой. Можно отметить частое использование комментариев и тегов типа XML внутри комментариев, они нужны для документации. Документация C# будет обсуждаться ниже в этом приложении:

public struct Wheels {

int wheel;

// загрузить начальное значение в wheel

private Wheels(int initVal); {

wheel = initVal;

}

/// <summary>

/// показывает внутреннее число wheels

/// </summary>

internal int Number {

set {

wheel = value;

}

get {

return wheel;

}

}

/// <summary>

/// возвращает внутреннее число. Если этот метод

/// не переопределен, то возвращаемой строкой будет тип Two.Wheels.

/// </ summary >

/// <returns></returns>

public override string ToString() {

return wheel.ToString();

}

/// < summary>

/// выполнить операцию сложения на двух wheels

/// </summary>

/// <param name="w1"></param>

/// <param name="w2"></param>

/// <returns></returns>

public static Wheels operator + (Wheels w1, Wheels w2) {

w1.wheel += w2.wheel; return w1;

}

/// <summary>

/// предоставляет альтернативную функциональность сложения.

/// отметим, что вторая альтернатива операции сложения

/// находится не в этой структуре, а в классе car

/// </summary>

/// <param name= "w"></param>

/// <returns></returns>

public Wheels AddWeels(Wheels w) {

this.wheel += w.wheel;

return this;

}

/// <summary>

/// поэтому целые литералы можно неявно преобразовать в wheel

/// </summary>

/// <param name="x"></param>

/// <returns></returns>

public static implicit operator Wheels(int x) {

return new Wheels(x);

}

}

Здесь выделим использование метода AddWheel(), что удовлетворяет рекомендациям об альтернативных сигнатурах. Язык CLS, который не поддерживает перезагрузку операторов, может получить доступ к той же функциональности сложения с помощью этого метода. Фрагмент кода ниже показывает, как может использоваться этот тип данных значения:

public static void Main(String[] args) {

Wheels front = 2; // неявное преобразование

Wheels back = 4; // неявное преобразование

Wheels total = front + back; // перезагруженная версия сложения

Console.WriteLine(total);

}

Компиляция и выполнение этого кода дадут в результате 6. Можно также изменить тип Car, чтобы разрешить сложение и вычитание из него Wheels. Следующий код показывает изменения, сделанные в классе Car:

public class Car {

int wheels, doors, headlights;

public Car(int wheels, int doors, int headlights) {

this.wheels = wheels;

this.doors = doors;

this.headlights = headlights;

}

public Car AddWheel(Two.Wheels w) {

this.wheels += w.Number;

return this;

}

internal int Wheels {

set {

wheels = value;

}

get {

return wheels;

}

}

/// <summary>

/// выполняет операцию сложения на Wheel и Car

/// </summary>

/// <param name="c1">car</param>

/// <param name="w1">wheel</param>

/// <returns></returns>

public static Car operator +(Car c1, Wheels w1) {

c1.Wheels += w1.Number;

return c1;

}

/// <summary>

/// выполняет операцию вычитания на Wheel и Car

/// </summary>

/// <param name="c1">car</param>

/// <param name="w1">wheel</param>

/// <returns></returns>

public static Car operator -(Car c1, Wheels w1) {

c1.Wheels -= w1.Number;

return c1;

}

public override string ToString() {

return

"[wheels = " + wheels + "| doors = " + doors + "|"

+ " headlights = " + headlights + "]";

}

}

В класс Car также был добавлен метод AddWheel. Представленный далее фрагмент кода проверяет функциональность, только что добавленную в Car:

public static void Main(String[] args) {

Wheels front = 2;

Wheels back = 4;

Wheels total = front + back;

Car greenFordExpedition = new Car(0, 4, 2);

Console.WriteLine("initial:\t" + greenFordExpedition);

greenFordExpedition += total;

Console.WriteLine("after add:\t" + greenFordExpedition);

greenFordExpedition -= front;

Console.WriteLine("after subtract:\t" + greenFordExpedition);

}

Компиляция и выполнение этого кода создадут приведенные ниже результаты:

initial:        CAR-[wheels = 0| doors = 4| headlights = 2 ]

after add:      CAR-[wheels = 6| doors = 4| headlights = 2 ]

after subtract: CAR-[wheels = 4| doors = 4| headlights = 2 ]

sizeof и typeof

Так как Java не имеет других типов данных значений, кроме примитивных, размер которых всегда известен, то реального применения для оператора sizeof нет. В C# типы данных значений охватывают примитивные типы, а также структуры и перечисления. Конечно, как и в Java, размер примитивных типов известен. Однако необходимо знать, сколько пространства занимает тип struct или enum, для чего служит оператор sizeof. Синтаксис достаточно простой: sizeof(<Value Type>), где <Value Type> будет struct или enum. Необходимо отметить один момент при использовании оператора sizeof — он может использоваться только в ненадежном контексте. Оператор sizeof не может быть перезагружен.

Оператор typeof используется для получения объекта типа экземпляра типа без создания экземпляра типа. В Java каждый тип имеет переменную класса public static, которая возвращает дескриптор объекта Class, ассоциированный с этим классом. Оператор typeof предоставляет функциональность этого типа. Так же как в случае sizeof, синтаксис будет очень простым: typeof(<Type>), где <Type> является любым типом, определенным пользователем, который вернет объект типа этого типа.

Делегаты

Делегаты являются членами пространства имен, которые инкапсулируют ссылку на метод внутри объекта делегата. Объект делегата может затем передаваться в код, вызывающий указанный метод, не зная во время компиляции, какой метод будет вызван. Красоту, мощь и гибкость делегатов можно увидеть только с помощью примера. Давайте посмотрим, как работают делегаты:

namespace Samples {

using System;

using System.Collections;

public delegate void TestDelegate(string k); // определяет делегата,

// который получает строку в качестве аргумента

public class Sample {

public Sample() {}

public void test(string i) {

Console.WriteLine(i + " has been invoked.");

}

public void text2(string j) {

Console.WriteLine("this is another way to invoke {0}" + j);

}

public static void Main(string[] args) {

Sample sm = new Sample();

TestDelegate aDelegate = new TestDelegate(sm.test);

TestDelegate anotherDelegate = new TestDelegate(sm.test2);

aDelegate("test");

anotherDelegate("test2");

}

}

}

Первый шаг по использованию делегатов состоит в определении делегата. Наш тестовый делегат определяется в строке public delegate void TestDelegate(string k); . Затем определяется класс с методами, которые имеют сигнатуру, аналогичную делегату. Конечный шаг заключается в создании экземпляра делегата который создается так же, как экземпляр класса и реализуется с помощью оператора new. Единственное различие состоит в том, что имя целевого метода передается делегату как аргумент. Затем вызывается делегат. В примере вызывается экземпляр aDelegate с помощью вызова aDelegate("test");.

Подробно о классах

Как в Java, как и в C#, класс является скелетом, который содержит методы, но не данные. Это структура потенциального объекта. Образование экземпляра класса создает объект на основе этой структуры. Существуют различные ключевые слова и концепции, связанные с классами, которые, были рассмотрены ранее. В этом разделе мы повторим эти ключевые слова и введем новые.

Модификаторы

Как и в Java, модификаторы в C# используются для модификации объявлений типа и членов. Далее представлен список модификаторов C#. Более подробное определение значений отдельных идентификаторов дано в разделе о ключевых словах данного приложения. Однако некоторые из перечисленных модификаторов являются новыми и будут рассмотрены в ближайших разделах.

Модификатор класса

Описание

abstract

Нельзя создавать экземпляры абстрактных классов. Производные классы, которые их расширяют, должны реализовать все абстрактные методы класса, и модификатор sealed нельзя применять к этим классам.

sealed

Используется для предотвращения случайного наследования, так как от класса, определенного как sealed, нельзя наследовать.

Модификатор члена

Цель

Эквивалент в Java

Описание

virtual

Методы, методы доступа

недоступно

Позволяет переопределять целевые члены классам-наследникам.

static

Все

static

Целевой член, помеченный как static, принадлежит классу, а не экземпляру этого класса. Поэтому не требуется создавать экземпляр класса, чтобы получить к нему доступ.

event

Поля, свойства

недоступно

Используемый для связывания клиентского кода с событиями класса, модификатор event позволяет определить делегата, который будет вызываться, когда в коде произойдет некоторое "событие". Отметим, что программист класса определяет, где и когда инициируется событие, а подписчик определяет, как его обработать.

abstract

Методы, методы доступа

abstract

Указывает, что целевой член является неявно виртуальным и не имеет кода реализации. Производный класс должен предоставить эту реализацию, при этом реализованный метод помечается как override.

const

Поля, локальные переменные

final

Указывает, что целевой член не может быть изменен. Java также имеет ключевое слово const, которое в данный момент является просто зарезервированным словом.

readonly

Поля

недоступно

Указывает, что целевому члену можно присвоить значение только при его объявлении или в конструкторе класса, содержащего этот член.

extern

Методы

недоступно

Указывает, что целевой член реализуется внешне. Этот модификатор обычно используется с атрибутом DllImport.

override

Методы

недоступно

Указывает, что целевой член предоставляет новую реализацию члена, унаследованного из базового класса.

Модификатор доступа

Цель

Эквивалент в Java

Описание

По умолчанию

public

Все

public

Без ограничений.

Члены enum и interface, а также пространства имен.

private

Все

private

Доступны только объявляющему классу.

Члены class и struct.

internal

Все

недоступно

Доступны файлам в той же сборке.

protected

Все

недоступно

Доступны для объявляющего класса и любых его подклассов. В C# protected более ограничен, чем в Java. Закрытый (protected) доступ не позволит другим файлам в той же сборке иметь доступ к члену.

protected internal

Все

protected

Доступны для файлов сборки и подклассов объявляющего класса.

Конструкторы

Первый метод, который будет вызван в классе в процессе создания экземпляра объекта,— это конструктор. Утверждение справедливо для Java, C++, C# и других языков. Фактически, даже если специально не писать свой собственный конструктор, будет создан конструктор по умолчанию. Но в C# обращение к объекту-предку или другому конструктору обрабатывается совершенно по-другому, чем в Java:

public class Parent {

}

public class Sample: Parent {

private string internalVal;

private string newVal;

public Sample():base() {}

public Sample(String s) {

internalVal = s;

}

public Sample(String s, String t) : this(s) {

newVal = t;

}

}

Из этого примера видно, что выполнение вызова конструктора предка или даже другого конструктора можно сделать, "расширяя" его с помощью символа ":". В случае конструктора предка используется ключевое слово base для идентификации источника, исходящего из объекта предка, в то время как это используется для идентификации источника, исходящего из другого конструктора объекта. Применение подходящей сигнатуры к base вызовет соответствующий конструктор предка, так же как применение правильной сигнатуры вызовет правильный внутренний конструктор. Мы подчеркнем это, делая некоторые изменения в класс Sample:

public class Parent {

protected Parent(string a) {

Console.WriteLine(a);

}

protected Parent() {

Console.WriteLine("This is the base constructor");

}

}

public class Sample: Parent {

public Sample() {

}

public Sample(String s):base(s) {

}

public Sample(String s, String t): this(s) {

Console.WriteLine(t);

}

}

C# вводит концепцию деструкторов, позаимствованную из C++. Они работают аналогично завершителям (finalizer) в Java, их синтаксис, однако, существенно отличается. Деструкторы используют логический знак отрицания (~) в качестве префикса для имени класса:

~Sample() {

}

Рекомендация в отношении кода деструктора: "сборщик мусора" в .NET не вызывается сразу же после того, как переменная покидает область действия. На самом деле имеется некоторый интервал времени или условия памяти, которые инициируют поток выполнения. Бывают случаи, когда деструктор запускается в условиях нехватки памяти, поэтому желательно делать его код как можно короче. Также неплохо вызывать close на объектах, использующих много ресурсов, прежде чем разрушить контроллеры, которые их применяют.

Методы

Java и C# существенно различаются в синтаксисе и идеологии в отношении способа, которым объект образовывает методы. Это связано с одной причиной — не все параметры типа ссылочных данных передаются как ссылки и не все простые типы данных должны передаваться по значению. Имеется возможность передавать аргументы по значению, как параметр in (это способ передачи параметров по умолчанию), по ссылке, как параметр ref, или как параметр out. Следующий код:

public static void Main(string[] args) {

int a = 10;

Console.WriteLine(a);

Add(a);

Console.WriteLine(a);

}

public static void Add(int a) {

a++;

}

будет создавать результат, показанный ниже, как в C#, так и в Java:

10

10

Мы передаем а по значению, поэтому это значение не связано со значением в Main. Следовательно, увеличение а в методе Add не влияет на а в Main. Используя возможность, позволяющую передавать простые типы данных как ссылки, приведенный выше код можно изменить следующим образом:

public static void Main(string[] args) {

int a = 10;

Console.WriteLine(a);

Add(ref a);

Console.WriteLine(a);

}

public static void Add(ref int a) {

a++;

}

и получить:

10

11

Чтобы использовать ссылочный параметр, надо перед типом параметра использовать ключевое слово ref. В противоположность двум другим типам параметров параметры out не нуждаются в инициализации, перед тем как они передаются в качестве аргументов, они используются для передачи значений назад из метода. Следующий код создаст результат 100:

public static void Main(string[] args) {

int a;

Add(out a);

Console.WriteLine(a);

}

public static void Add(out int a) {

a = 100;

}

Еще одним удачным способом в C# является сокрытие метода. Концепция сокрытия метода обсуждалась ранее в этом приложении. Она позволяет иметь такую же сигнатуру, как и у метода базового класса, не переопределяя базовый метод. Это делается с помощью ключевого слова new, которое помещается перед реализацией метода. Отметим, что, как описано ранее, отсутствие ключевого слова new в экземпляре this по прежнему создаст то же поведение и не будет вызывать ошибки компиляции, будет создано только предупреждение. Однако, лучше его использовать, по крайней мере для того, чтобы знать, где сталкиваются сигнатуры этих методов. Вот пример сокрытия метода:

namespace Sample {

using System;

public class SuperHider {

public string Test() {

return "parent test";

}

}

public class Hider: SuperHider {

public Hider() {

}

new public string Test() {

return "child test";

}

}

}

Следующий листинг показывает, как вызывается любая версия метода Test():

Rider hider = new Hider();

Console.WriteLine(hider.Test());

Console.WriteLine(((SuperHider)h).Test());

Результатом этих вызовов будет:

Child test

Parent test

Сокрытие методов существенно отличается от переопределения методов. В C# переопределение метода является явной процедурой. Это отличается от подхода Java, где переопределение является поведением по умолчанию, когда сигнатура члена суперкласса совпадает с сигнатурой в его подклассе. Чтобы переопределить метод базового класса в C#, необходимо пометить его как virtual. К счастью нельзя просто изменить класс Hider, что показано в данном примере:

namespace Samples {

using System; public class SuperHider {

public string Test() {

return "parent test";

}

}

public class Hider: SuperHider {

public Hider() {

}

public override string Test() {

return "child test";

}

}

}

Этот код не будет компилироваться. Надо сначала проинформировать компилятор, что указанный метод, в данном случае SuperHider.test(), может быть переопределен классами потомками. Для этого в C# используется ключевое слово virtual, а методы, к которым применяется этот модификатор, называются виртуальными методами. Возьмем пример подходящего способа выполнения переопределения метода:

namespace Samples {

using System;

public class SuperHider {

public virtual string Test() {

return "parent test";

}

}

public class Hider: SuperHider {

public Hider() { }

public override string Test() {

return "child test";

}

}

}

Достоинством переопределения метода является гарантия, что будет вызван самый производный метод. Взгляните на код вызова, представленный ниже, такой же код, что и в примере сокрытия метода, создает два других значения:

Hider hider = new Hider();

Console.WriteLine(hider.Test());

Console.WriteLine(((SuperHider)hider).Test());

Так как гарантировано, что всегда вызывается версия test из Hider, мы знаем, что компиляция и выполнение кода всегда дадут следующие результаты:

Child test

Child test

Единственное синтаксическое различие между абстрактными классами в Java и C# состоит в размещении ключевого слова abstract. Как и в Java, определение абстрактных методов в C# делает класс абстрактным.

Свойства и индексаторы

Раньше методы get() и set() использовались для доступа к внутренним атрибутам объекта. Сегодня C# вводит концепцию аксессоров (accessor), которые предоставляют безопасный и гибкий способ получения внутри лих полей, Существует два типа аксессоров. Аксессор get разрешает чтение внутренних полей объекта, а аксессор set позволяет изменять значение внутреннего поля. Ключевое слово value представляет новое значение справа от знака равенства во время присваивания. Отсутствие соответствующего аксессора в объявлении свойства приведет к тому, что свойство будет предназначаться либо только для чтения (нет set), либо только для записи (нет get):

namespace Samples {

using System;

public class Properties {

private int age;

private string name;

public Properties(string name) {

this.name = name;

}

public int Age {

get {

return age;

}

set {

age = value;

}

}

public string Name {

get {

return name;

}

}

}

}

В указанном примере свойство Age имеет аксессоры get и set, поэтому можно читать или записывать в это свойство. Свойство Name, однако, создается один раз, когда создается новый экземпляр объекта свойств, после чего можно только прочитать значение Name. Свойства доступны, как если бы они были открытыми полями:

Properties props = new Properties("john");

props.Age = 21;

Console.WriteLine("My name is {0}, and I am {1} years old.", props.Name, props.Age);

Результатом этого кода является:

My name is john, and I am 21 years old.

Примечание. Имена свойств должны быть уникальными.

Как предполагает название, индексаторы позволяют легко индексировать атрибуты объектов. Предположим, что нам необходимо предоставить аналогичную функциональность, не создавая двух отдельных свойств. Можно проиндексировать имеющиеся поля, чтобы они были доступны с помощью некоторого ключа (ключ является значением, используемым для доступа к индексу, для массивов ключ является целым значением), или извлечь объявления двух свойств в примере выше и заменить их следующим:

public string this[string a] {

get {

if (a.Equals("Age")) return int.ToString(age);

else if (a.Equals("Name")) return name;

else {

throw new Exception("can only accept 'name' or 'age' key");

}

}

set {

if (a.Equals("Age")) age = int.Parse(value);

else {

throw new Exception(a + " is read only or does not exist");

}

}

}

Затем можно обратиться к атрибутам свойств следующим образом:

Properties props = new Properties("john");

props["Age"] = "21";

Console.WriteLine("my name is {0}, I am {1} years old.", props["Name"], props["Age"]);

В результате мы получим:

My name is john, I am 21 years old.

События

События C# предоставляют значительно более надежный и гибкий паттерн наблюдателя, чем в Java. Более того, они могут быть объявлены либо как поля, либо как свойства. Создание события является процессом из трех частей. Сначала мы получаем делегата, а затем событие, связанное с этим делегатом, и наконец, когда происходит некоторое действие, вызывается событие.

Проиллюстрируем это, удаляя исключения из класса Properties и используя вместо этого события. Наиболее привлекательным свойством событий является гибкость, которая наблюдается при использовании модели делегата. По сути можно использовать любую специальную сигнатуру, которую желательно связать с событием, а затем подписчик на событие должен предоставить целевой метод, который соответствует требуемому списку параметров. Создание этой специальной сигнатуры начинается с определения делегата в коде пространства имен. Для класса Properties необходимо, чтобы он инициировал строковые события, аналогичные исключениям, которые необходимо порождать:

public delegate void ObservableDelegate(string message);

Затем объявляется delegate как поле event в классе:

public event ObservableDelegate ExceptionEventListener;

Наконец, переписывается реализация индексатора для активизации приемника события всякий раз, когда возникает условие исключения:

public string this[string а] {

get {

if (a.Equals("Age")) {

return int.ToString(age);

} else if (a.Equals("Name")) {

return name;

} else {

ExceptionEventListener(can only accept 'name' or 'age' key");

return null; // поток программы продолжается после того, как

// событие было инициировано, поэтому необходимо

// вернуть значение. В этом случае, так как ключ

// является недействительным (не 'name' или 'age'),

// возвращается null, указывая отсутствие значения

}

}

set {

if (a.Equals("Age") {

age = int.Parse(value);

}

else {

listener(a+ " is read only or does not exist");

}

}

}

Экземпляр делегата, связанный с приемником событий, никогда не создается. Это объясняется тем, что создание экземпляра реально происходит на клиенте, который потребляет это событие. Для клиента событие представляется как открытое поле, но не надо думать, что оно не имеет ограничений. Единственными возможными действиями по отношению к полю события являются:

□ Создание в событии новых экземпляров делегатов

□ Удаление экземпляров делегатов из события

C# использует операторы += и -= соответственно для добавления и удаления экземпляров делегатов из событий. Оба оператора одинаковы в C# и Java. Недопустимо задавать событие, равным одному любому экземпляру делегата. Вместо этого можно добавить в событие столько делегатов, сколько понадобится. Это свободно транслируется в требуемое количество приемников событий для одного события. Пример ниже показывает, как это можно сделать:

public delegate void TestEvent();

public class Tester {

public event TestEvent testEvent;

Tester() { }

public void Perform() {

testEvent();

}

public class Client {

Client() {

Tester tester = new Tester();

TestEvent a = new TestEvent(Callback1); // сначала создать делегата

tester.testEvent += a; // затем добавить его

tester.testEvent += new Test(CallBack2); // или можно сделать это

// за один проход

tester.testEvent += new Test(Callback3);

tester.testEvent += new Test(Callback4);

tester.Perform();

}

public void CallBack1() {

// уведомить через e-mail)

}

public void CallBack2() {

// послать факсы

}

public void CallBack3() {

// послать беспроводное сообщение

}

public void CallBack4() {

// сохранить в журнале

}

}

}

Как можно понять из приведенного примера, чтобы использовать события класса, необходимо сначала получить метод в классе подписчиков для обработки события (очень похоже на то, как действуют делегаты), затем добавить методы обработки события в событие. Давайте создадим статический метод Notify():

public static void Notify(string i) {

Console.WriteLine(i);

}

Этот метод использует такую же сигнатуру, что и приемник событий класса Properties. В методе Main можно зарегистрировать метод Notify() и задать условно ошибку, чтобы протестировать событие:

Properties props = new Properties("hello"); // зарегистрировать обработчик событий

props.ExceptionEventListener += new ExceptionEventListener(test);

p["Aged"] = "35"; // неправильный ключ используется

// для моделирования ошибки

Исключения

Исключения в C# на первый взгляд являются такими же, как в Java. Инструкции C# try-catch и try-catch-finally работают подобно своим аналогам в Java (смотрите раздел о ключевых словах). Однако в C# нельзя использовать инструкцию throws, поэтому невозможно указать вызывающей стороне, что некоторый код в методе может порождать исключение. Также имеется try-finally, который не подавляет порожденные исключения, но предлагает блок finally, выполняющий после порождения исключения, чтобы произвести очистку.

Порождение исключений делается с помощью инструкции throw. Например, чтобы породить SystemException, используется код throw new SystemException (<arg-list>);. Это полностью совпадает c тем, как исключения порождается в Java. Требуется только инструкция throws и подходящий класс исключения. Далее представлен список некоторых стандартных классов исключений, предоставляемых средой выполнения .NET. Так же как в Java, их функциональность отражается в данных им именах:

□ Exception — базовый класс для всех объектов исключений.

□ SystemException — базовый класс для всех ошибок, создаваемых во время выполнения.

□ IndexOutOfRangeException возникает, когда индекс массива во время выполнения оказывается вне границ заданного диапазона.

□ NullReferenceException порождается, когда во время выполнения ссылаются на null.

□ InvalidOperationException порождается некоторыми методами, когда вызов метода бывает не действителен для текущего состояния объекта.

□ ArgumentException — базовый класс всех исключений для аргументов.

□ ArgumentNullException порождается если аргумент задан как null, когда это недопустимо.

□ InteropException является базовым классом для исключений, которые возникают или направлены на среды вне CLR.

Одним исключением, которое возникает независимо от того, будет ли оно специально порождаться или нет, является System.OverflowException, связанное с вычисленными результатами, превосходящими диапазон значений типа данных переменной результата. Инструкции checked и unchecked могут инициировать или подавлять связанные с этим исключения. Дополнительная информация о checked и unchecked находится в разделе данного приложения о ключевых словах.

Условная компиляция

Препроцессор в C# эмулируется. Он выполняется как отдельный процесс, прежде чем компилятор начнет свою работу. Поддерживаемые здесь директивы больше всего соответствуют C++, чем какому-либо другому языку. Конечно, в Java не существует эквивалентов функциональности, описанных в этом разделе. Разрешается определять символы, которые проверяются с помощью простых условных директив. Те, которые оказываются true, включаются и компилируются, иначе код игнорируется. Определение символа может происходить двумя способами. Прежде всего с использованием ключа компилятора /define, за которым следует двоеточие и определяемый символ, например:

csc /define:TEST_TEST samples.cs

Можно также определить символы на странице конфигурационных свойств проекта, добавляя их в разделенный двоеточиями список констант условной компиляции. Или пойти программным путем, используя директиву #define. В этом случае директива должна появиться раньше, чем что-либо другое, и применяется ко всем лексемам в области действия файла. Здесь перечислены допустимые условные директивы:

□ #if используется для проверки существования символа

□ #elif позволяет добавить несколько ветвей к инструкции #if

□ #else предоставляет окончательное альтернативное условие для #if и #elif

□ #endif заканчивает инструкцию #if

namespace Samples {

using System;

#if EXAMPLE

public class Example {

public Example() {

}

}

#elif TEST_TEST

public class Test {

public Test() {

}

}

#else

public class None {

public None() {

}

}

#endif

}

Добавление инструкции #define TEST_TEST делает класс Test видимым для компиляции, a #define EXAMPLE делает класс Example видимым для компиляции. Если ничего не добавлять, то будет компилироваться класс None. Для препроцессора C# доступны также две другие директивы — #warning и #error. Они должны помещаться в условное выражение #if. Попробуйте добавить следующую строку под инструкцией #else в приведенный код:

#warning I wouldn't try to instantiate the example object if I were you

C# также поддерживает условную функциональность. В коде ниже добавление условного атрибута в AMethod() делает его компилируемым только в том случае, когда определен символ Test:

[conditional("TEST_TEST")]

public void AMethod() {

string s = "I am available only when Test is defined";

}

Вопросы безопасности

В настоящее время код может приходить из разных источников. В Java, до появления Java 2, существовала установка, что все приложения, которым разрешается использовать все свойства языка, должны быть абсолютно надежными. Последующий опыт показал, что такой подход может быть достаточно опасным. Java теперь предоставляет службы политики безопасности с помощью файла java.policy. Приложения подвергаются той же проверке безопасности, что и апплеты. Политика безопасности может редактироваться напрямую или через policytool для создания приложений, в которых есть ограничения. Среда .NET для решения этой проблемы использует систему безопасности доступа к коду, которая контролирует доступ к защищенным ресурсам и операциям. Ниже представлен список наиболее важных функций системы безопасности доступа к коду:

□ Код может требовать, чтобы вызывающая сторона имела специальные полномочия.

□ Выполнение кода ограничено временем выполнения, в этом случае проводятся проверки, которые определяют, соответствуют ли предоставленные полномочия вызывающей стороны требуемым полномочиям операций.

□ Код может запрашивать полномочия, которые ему требуются для выполнения и полномочия, которые будут полезны, а также явно утверждать, какие полномочия он никогда не должен иметь.

□ Определяются полномочия, которые представляют определенные права для доступа к различным системным ресурсам.

□ Администраторы могут выбирать политику системы безопасности, которая присваивает определенные полномочия определенным группам кода.

□ Система безопасности доступа к коду предоставляет полномочия, когда компонент загружается. Это предоставление основывается на запросе кода, а также на операциях, разрешенных системой безопасности.

Обеспечение политики безопасности делает надежной среду управляемого кода .NET. Это объясняется тем, что каждая загружаемая сборка подчиняется политике безопасности, которая предоставляет полномочия для кода на основе доверия, где доверие базируется на признаках данного кода. Система безопасности .NET позволяет коду использовать защищенные ресурсы, только если он имеет на это "полномочие". Код запрашивает полномочия, которые ему требуются, а политика безопасности, применяемая .NET, определяет, какие полномочия будут реально предоставлены коду. Среда .NET предоставляет в C# классы полномочий доступа к коду, каждый из которых инкапсулирует возможность доступа к определенному ресурсу. Связанный с каждым полномочием класс является перечислением флагов полномочий, используемых для определения конкретного флага полномочий доступа к объекту. Эти полномочия используются для указания .NET, что надо разрешить коду делать и что должно быть разрешено вызывающей этот код стороне. Политика использует эти объекты также для определения, какие полномочия дать коду. Далее следует список стандартных полномочий:

□ EnvironmentPermission: определяет полномочия доступа для переменных окружения. Существуют два возможных типа доступа — только для чтения и только для записи. Доступ для записи предоставляет также полномочия для создания и удаления переменных окружения.

□ FileIOPermission: существуют три возможных типа полномочий ввода/вывода для файлов — чтение, запись и добавление. Чтение и запись самоочевидны, добавление ограничивается только добавлением, что читать остальное не разрешается.

□ ReflectionPermission: управляет возможностью чтения информации о типе неоткрытых членов типа, а также использованием Reflection.Emit.

□ RegistryPermission: управляет чтением, записью и созданием в реестре.

□ SecurityPermission: управляет совокупностью флагов полномочий, используемых системой безопасности.

□ UIPermission: управляет доступом к различным аспектам интерфейса пользователя.

□ FileDialogPermission: управляет доступом к файлам на основе диалогового окна системного файла.

□ IsolatedStoragePermission: управляет доступом к изолированной памяти.

В C# существуют два способа изменения текущих полномочий безопасности с помощью использования: вызовов для классов полномочий в средах .NET или атрибутов полномочий безопасности.

Заключение

Microsoft описывает C# как простой, современный, объектно-ориентированный и обеспечивающий безопасность типов язык программирования, производный из С и C++. Так как Java также является модернизацией C++, большая часть синтаксиса и встроенных свойств, представленных в C#, также доступны в Java.

C# использует среду .NET и поэтому предлагает встроенный, обеспечивающий безопасность типов, объектно-ориентированный код, взаимодействующий с любым языком, который поддерживает CTS (общую систему типов). Java может работать с С и C++, но без обеспечения безопасности типов. Более того, это достаточно сложно. В то же время C# предоставляет перезагрузку операторов, a Java этого не делает.

Имена файлов в C# не связаны с классами в них, как это предусмотрено в Java, а также имена пространств имен не связаны с папками, как имена пакетов в Java. C# вводит концепцию делегатов — указателей функций, которые могут использоваться для инкапсуляции метода с определенной сигнатурой. Также C# предлагает множество встроенных типов данных значений, включая обеспечивающие безопасность типов перечисления, структуры и встроенные примитивы, которые представляют достойную альтернативу примитивам Java.

В C# реализовано двунаправленное преобразование между ссылками и типами данных значений, называемое упаковкой и распаковкой. Эта функциональность не поддерживается в Java. C# поддерживает использование классов, укомплектованных полями, конструкторами и методами в качестве шаблонов для описания типов данных, и предоставляет возможность определить деструкторы — методы, вызываемые перед тем, как класс попадает к сборщику мусора. C# предоставляет также три подхода к параметрам методов — in, out или ref, где по умолчанию используется in.

C# вводит также концепцию сокрытия методов, а также поддержку явного переопределения с помощью ключевых слов virtual и override. В C# предусмотрены свойства как альтернатива методам getXXX() и setXXX(), обеспечивающие способ безопасного доступа к внутренним полям. Кроме того, C# допускает создание индексаторов для предоставления индексного доступа к внутренним полям объекта. В отличие от Java, однако, в C# нет возможности объявить, что метод может порождать исключение.

Пространства имен C# предоставляют значительно более гибкий способ группирования связанных классов. В C# возможна эмуляция препроцессора, код условно включается или исключается на основе существования определенных символов. C# предоставляет модель безопасности на основе полномочий, которой можно управлять программным путем.

Приложение C

C# для разработчиков VB6

В этом приложении будет представлено краткое введение в язык C#, специально предназначенное для тех разработчиков, опыт которых до сих пор был частично или полностью связан с Visual Basic 6.

Отметим, что в рамках этого приложения аббревиатура VB означает VB6. В тех случаях, когда говорится о VB.NET, это указывается явно.

C# и Visual Basic являются очень разными языками как в своих синтаксических стилях, так и в фундаментальных концепциях, на которых они основываются. Это означает, что разработчикам Visual Basic потребуются существенные усилия, чтобы освоиться с C# даже на базовом уровне. С помощью этого приложения мы попытаемся облегчить процесс обучения, предоставив введение в C#, которое специально предполагает знание VB, и сосредоточившись на основных концептуальных различиях между двумя языками. В ходе этого приложения будет проведено сравнение, как кодировать решения проблемы в VB и в C#, представляя вместе код C# и код VB.

Это означает, что рассмотрение языка C# ограничится базовым уровнем, здесь не будут рассмотрены более развитые свойства языка, им посвящены учебные главы основного текста книги. Мы сосредоточим внимание на различных методологиях, вовлеченных в написание кода с помощью языка C#.

Различия между C# и VB

Помимо очевидных синтаксических различий между языками, существуют две основные концепции, которые необходимо знать, чтобы можно было двигаться от VB к C#:

1. Концепция полного потока выполнения программы от начала до конца. Visual Basic скрывает этот аспект программ, так что в модулях классов кодируется только часть программы VB, связанная с обработкой событий и всеми методами. C# делает доступной всю программу в виде исходного кода. Это объясняется тем, что C# можно философски рассматривать как последующую генерацию C++, а корни C++ уходят в 60-е годы. C++ предшествовал оконным интерфейсам пользователя и развитым операционным системам. C++ развился как низкоуровневый, близкий к машине универсальный язык программирования. Написать приложение GUI с помощью C++ означает, что необходимо явно вызвать системные вызовы для создания и взаимодействия с оконными формами. Язык C# построили на этом наследии, упростив и модернизировав C++ так, чтобы низкоуровневые преимущества C++ в производительности можно было достичь с помощью кодирования не сложнее, чем в VB. VB, с другой стороны, является молодым языком, созданным специально для быстрой разработки приложений GUI Windows. По этой причине в VB весь стандартный код GUI является скрытым, и программист VB реализует только обработчики событий. C#, со своей стороны, показывает стандартный код как часть исходного кода.

2. Классы и наследование C# значительно более объектно-ориентированны, чем в VB, при условии, что весь код является частью класса. Он обеспечивает также исчерпывающую поддержку наследования реализации. На самом деле большинство хорошо спроектированных программ C# будут в значительной степени отвечать этой форме наследования, которая полностью отсутствует в VB.

Основная часть этого приложения посвящена разработке двух примеров, для которых будут закодированы версии на VB и C#. Первый пример является простой формой, запрашивающей у пользователя число и выводящей квадратный корень и знак числа. Сравнивая подробно версии примера на VB и C#, мы увидим базовый синтаксис C# и поймем концепции, которые лежат в основе потока выполнения программы.

Затем будет представлен модуль класса VB, который хранит информацию о сотрудниках, и его эквивалент в C#. Здесь мы начнем знакомиться с реальной мощью C#, так как при добавлении свойств в примеры быстро станет понятно, что VB просто не поддерживает концепции, которые нужны для разработки модуля класса согласно заданным требованиям, и придется продолжать только на C#.

Приложение будет закончено кратким обзором некоторых из оставшихся различий между VB и C#, не показанных в примерах.

Однако прежде чем начать, необходимо разобрать несколько концепций, касающихся классов, компиляции и базовых классов .NET.

Классы

В этом приложении будут достаточно интенсивно использоваться классы C#. Они представляют собой точно определенные объекты, которые подробно рассматриваются в главах 4 и 5. Но для логичности изложения лучше представлять их как эквиваленты C# модулей классов VB, так как они являются достаточно похожими сущностями. Подобно модулю класса VB, класс C# реализует свойства и методы и содержит переменные члены. Так же как для модуля класса VB, можно создавать объекты заданного класса C# (экземпляры класса) с помощью оператора new. За этим сходством, однако, скрывается много различии. Например, модуль класса VB является на самом деле классом COM. Классы C#, напротив, обычно не являются классами COM, но они всегда интегрированы в среду .NET. Классы C# являются также более динамичными чем их аналоги VB/COM, в том смысле, что они обеспечивают более высокую производительность и дают меньшую потерю быстродействия при создании экземпляра. Но эти различия мы почти не будем учитывать при обсуждении языка C#.

Компиляция

Вы наверняка хорошо знаете, что компьютер никогда не выполняет напрямую код на любом языке высокого уровня, будь это VB, C++, С или любой другой язык. Вместо этого весь исходный код сначала транслируется в собственный исполнимый код машины с помощью процесса, обычно называемого компиляцией. При отладке VB предлагает возможность просто выполнить код сразу (то есть, когда каждая строка кода VB компилируется, или в этом случае говорят, что код интерпретируется, так как компьютер готов выполнить эту строку кода) либо произвести полную компиляцию (так, что вся программа сначала транслируется в исполнимый код, а затем начинается его реализация). Выполнение в начале полной компиляции означает, что все синтаксические ошибки обнаруживаются компилятором до того, как программа заработает. Это ведет к более высокой производительности и допускается только в C#.

В C# компиляция делается в два этапа, где первый этап выполняется на так называемом промежуточном языке (IL) Этот этап будем называть собственно компиляцией. Второй этап — преобразование в исполнимый код, происходящее во время выполнения, является более простым этапом, поэтому он не ведет к значительным потерям производительности. Он также не является интерпретацией. Сразу целые части кода преобразуются из IL в язык ассемблера, и полученный исполнимый код на собственном языке машины затем сохраняется, поэтому не требуется его перекомпиляции в следующий раз, когда будет выполняться эта часть кода. Компания Microsoft считает, что в комбинации с различными оптимизациями это, в конечном счете ведет к коду, который действительно выполнится быстрее, чем при использовании предыдущей системы прямой компиляции из исходного кода в собственный исполнимый код. Хотя о существовании IL необходимо помнить, он не будет влиять на обсуждение в этом приложении, так как он реально не влияет на синтаксис языка C#.

Базовые классы .NET

VB не состоит просто из одного языка. Существует большое число связанных с ним функций, скажем, функции преобразования CInt, CStr и т.д., функции файловой системы, функции даты-времени и многие другие. VB также полагается на присутствие элементов управления ActiveX, предоставляющих стандартные элементы управления, которые помещаются на форме,— поля списков, кнопки, текстовые поля и т.д.

C# также полагается на интенсивную поддержку из областей такого вида, но в случае C# поддержка предоставляется с помощью большого множества классов, известного как базовые классы .NET. Эти классы оказывают поддержку почти всем аспектам разработок под Windows. Существуют классы, представляющие все обычные элементы управления, дату, время, доступ к файловой системе, доступ в Интернет, а также многие другие. Здесь подробно не рассматривается библиотека базовых классов .NET, но она часто используется. На самом деле C# очень хорошо интегрирован в пазовые классы .NET, и можно обнаружить, что многие ключевые понятая C# — это просто оболочки определенных базовых классов. В частности, все базовые типы данных C#, которые используются для представления целых чисел, чисел с плавающей точкой, строк и т. д. являются на самом деле базовыми классами.

Одним из важных различий между VB6 и C# в этом отношении является то, что система функций VB является специфической для VB, в то время как базовые класс .NЕТ доступны для любого поддерживающего .NET языка программирования.

Соглашения

В этом приложении код на C# часто сравнивается с Visual Basic. Чтобы облегчить идентификацию кода на двух языках программирования, код C# представлен в том же формате, что и в приложениях В и C:

// Код который уже был показан

// Код C#, на который необходимо обратить внимание или который

// является новым

Однако весь код VB дается в следующем формате:

' Код VB представлен на белом фоне

Пример: Форма для извлечения квадратного корня

В этом разделе мы рассмотрим простое приложение, называемое SquareRoot, которое будет разработано на Visual Basic и на C#. Приложение является обычным диалоговым окном, которое приглашает пользователя ввести число и затем, когда пользователь нажимает кнопку, выводит знак и квадратный корень этого числа. Если число отрицательное, то квадратный корень необходимо вывести как комплексное число, что просто означает извлечение квадратного корня из обратного числа и добавление после него 'i'. Версия C# примера выглядит следующим образом. Версия VB по виду почти идентична, за исключением того, что имеет стандартною пиктограмму VB вместо пиктограммы оконных форм .NET в верхнем левом углу:

Версия SquareRoot на VB

Чтобы это приложение работало в Visual Basic, надо добавить обработчик событий для события нажатия кнопки. Для кнопки задается имя cmdShowResults, а текстовые поля имеют имена txtNumber, txtSign и txtResult. С этими именами обработчик событий выглядит следующим образом:

Option Explicit

Private Sub cmdShowResults_Click()

Dim NumberInput As Single

NumberInput = CSng(Me.txtNumber.Text)

If (NumberInput < 0) Then

Me.txtSign.Text = "Negative"

Me.txtResult.Text = CStr(Sqr(-NumberInput)) & " i"

ElseIf (NumberInput = 0) Then

txtSign.Text = "Zero"

txtResult.Text = "0"

Else

Me.txtSign.Text = "Positive"

Me.txtResult.Text = CStr(Sqr(NumberInput))

End If

End Sub

Это единственный фрагмент кода VB, который необходимо написать.

Версия SquareRoot на C#

На C# также необходимо написать обработчик событий для события нажатия кнопки. Здесь сохраняются те же имена кнопки и текстовых полей, но на C# код выглядит следующим образом:

// Обработчик событий нажатия пользователем кнопки Show Results.

// выводится квадратный корень и знак числа

private void OnClickShowResults(object sender, System.EventArgs e) {

float NumberInput = float.Parse(this.txtNumber.Text);

if (NumberInput < 0) {

this.txtSign.Text = "Negative";

this.txtResult.Text = Math.Sqrt(-NumberInput).ToString() + " i";

} else if (NumberInput == 0) {

txtSign.Text = "Zero";

txtResult.Text = "0";

} else {

this.txtSign.Text = "Positive";

this.txtResult.Text = Math.Sqrt(NumberInput).ToString();

}

}

Сравнивая эти два примера кода, можно увидеть сходство в структуре кода и даже без всякого знания C# получить представление о том, что происходит. Также понятно, что существует множество различий в синтаксисе двух языков. Далее будет проведено сравнение этих примеров, чтобы детально обсудить синтаксис C#. В ходе этого процесса мы также выявим различия между базовыми методологиями C# и VB.

Базовый синтаксис

Давайте рассмотрим две программы SquareRoot для ознакомления с синтаксисом C#.

С# требует, чтобы все переменные были объявлены

Начнем с первой строки кода VB, где находится объявление Option Explicit. Эта инструкция не имеет аналога в C#, так как в C# переменные должны всегда быть объявлены до своего использования. Это соответствует тому, как если бы C# всегда выполнялся с включенным Option Explicit и не разрешал отключить этот режим. Поэтому нет необходимости явно объявлять Option Explicit.

Причина такого ограничения заключается в том, что C# был очень тщательно спроектирован таким образом, чтобы затруднить случайное создание ошибок в коде. В VB рекомендуют всегда использовать Option Explicit, потому что это препятствует созданию трудно находимых ошибок, вызываемых неправильно записанными именами переменных. Легко заметить, что C# не позволяет делать вещи, которые с большой вероятностью могут привести к ошибкам.

Комментарии

Комментирование кода всегда важно, поэтому дальше в обоих примерах (или первое, что делается в примере на C#) добавляется комментарий:

// Обработчик событий нажатия пользователем кнопки Show Results.

// Выводится квадратный корень и знак числа

private void OnClickShowResults(object sender, System.EventArgs e) {

В VB для обозначения начала комментария используется апостроф, а комментарий продолжается до конца строки. Комментарии в C# в этом коде действуют таким же образом, за исключением того, что начинаются с двух прямых наклонных черт: //. Также как и для комментариев VB, можно использовать всю строку или добавить комментарий в конце строки:

// Этот код определяет результаты

int Result = 10 * Input; // получение результата

Однако C# более гибок в своих комментариях, так как позволяет использовать два других способа указания комментариев, каждый из которых имеет слегка различный эффект.

Комментарий также может быть ограничен последовательностями символов /* и */. Другими словами, если компилятор встречает последовательность /*, он предполагает, что весь последующий текст является комментарием, пока не встретит последовательность */. Это позволяет иметь длинные комментарии, которые распространяются на несколько строк:

/* этот текст действительно является длинным

длинным

длинным

длинным

комментарием * /

Короткие комментарии внутри строки являются очень полезными, если необходимо только временно заменить что-то в строке во время отладки:

X = /* 20 */ 15;

Третий способ похож на первый. Однако теперь используется три слэша:

/// <summary>

/// Event handler for user clicking Show Results button.

/// Displays square root and sign of number

/// </summary>

/// <param name="sender"></param>

/// <param name="e"></param>

private void OnClickShowResults(object sender, System.EventArgs e)

Если впереди используются три наклонные черты вместо двух, то комментарий по-прежнему продолжается до конца строки. Однако этот комментарий имеет теперь дополнительный результат: компилятор C# способен на самом деле использовать комментарии, которые начинаются с трех наклонных черт, чтобы автоматически создавать документацию для исходного кода как отдельный файл XML. Именно поэтому пример выше имеет достаточно формальную структуру для реального текста комментария. Эта структура готова к размещению в файле XML. Здесь не будут представлены детали этого процесса (он рассмотрен в главе 3). Необходимо только сказать, что комментируя каждый метод в коде, можно автоматически получить законченную документацию, которая обновляется при изменении кода. Компилятор будет даже проверять, что документация соответствует сигнатурам методов и т.д.

Разделение и группировка инструкций

Наиболее заметным различием между приведенными выше кодами на VB и на C# будет, почти наверняка, присутствие точек с запятыми и фигурных скобок в коде C#. Хотя это делает код C# довольно устрашающим, принцип на самом деле очень простой. Visual Basic применяет возврат каретки для указания конца инструкции, в то время как в C# используется для той же цели точка с запятой. Фактически компилятор полностью игнорирует все лишние пробелы, включая возвраты каретки. Эти свойства синтаксиса C# можно комбинировать, чтобы предоставить большую свободу при размещении кода. Например, следующий код (переформатированный из части приведенного выше примера) также вполне допустим в C#:

this.txtSign.Text = "Negative";

this.txtResult.Text = Math.Sqrt(-NumberInput) + " i";

Хотя очевидно, что если потребуется, чтобы другие люди смогли прочитать код, лучше предпочесть первый стиль кодирования. Visual Studio.NET будет в любом случае автоматически размещать код в таком стиле.

Скобки используются для группирования вместе инструкций в так называемые блочные инструкции (или иногда составные инструкции). Эта концепция в действительности не существует в VB. Можно сгруппировать вместе любые инструкции, помещая вокруг них скобки. Группа теперь рассматривается как одна блочная инструкция и может использоваться в любом месте в C#, где ожидается одиночная инструкция.

Блочные инструкции часто используются в C#. Например, в приведенном выше коде C# не существует явного указания на конец метода (C# имеет методы там, где VB имеет функции и подпрограммы). VB требуется инструкция End Sub в конце любой подпрограммы, так как подпрограмма может содержать сколько угодно инструкций. Специальный маркер является единственным способом, который известен в VB для определения конца подпрограммы. C# действует по-другому. В C# метод формируется в точности из одной составной инструкции. В связи с этим он заканчивается закрывающей фигурной скобкой, соответствующей открывающей скобке в начале метода.

Следующее действие часто встречается в C#: там, где Visual Basic использует некоторое ключевое слово для пометки конца блока кода, C# просто объединяет блок в составную инструкцию. Инструкция if в приведенных выше примерах иллюстрирует такой подход. В VB необходима инструкция EndIf для отметки окончания блока. В C# правило состоит в том, что предложение if всегда содержит точно одну инструкцию, и предложение else тоже одну. Если необходимо поместить более одной инструкции в любом предложении, как в случае примера выше, используется составная инструкция.

Использование заглавных букв

Еще одна особенность, которую можно заметить в отношении синтаксиса, состоит в том, что все ключевые слова— if, else, int и т.д. в коде C# пишутся со строчной буквы. В отличие от VB язык C# различает заглавные и строчные буквы. Если написать If вместо if, то компилятор не поймет этот код. Одним из преимуществ использования заглавных и строчных букв является возможность существования двух переменных, имена которых различаются только регистром символов, таких как Name и name. Такая ситуация встретится во втором примере этого приложения.

Идея имен переменных, различающихся только регистром символов, может показаться странной на первый взгляд. Но если к этому привыкнуть, можно обнаружить, что дополнительная свобода, которая благодаря этому достигается в именовании переменных, делает такую возможность действительно полезной.

Как правило, ключевые слова C# записываются полностью строчными буквами.

Методы

Давайте сравним синтаксис, применяющийся в VB и C# для объявления части кода, которая будет обрабатывать событие.

В VB:

Private Sub cmdShowResults_Click()

а в C#:

private void OnClickShowResults(object sender, System.EventArgs e)

Первое замечание, которое необходимо сделать, состоит в том, что версия VB объявляет подпрограмму, в то время как версия C# объявляет метод. В Visual Basic код традиционно группируется в подпрограммы и функции с лежащей в основе этого концепцией процедуры. Помимо этого, объекты классов VB имеют так называемые методы, которые для всех практических целей означают то же самое, что и процедуры, за исключением того, что они являются частью модуля класса.

C#, наоборот, обладает только методами (в соединении с тем фактом, что, как будет показано позднее, все в C# является частью класса). В C# нет отдельной концепции функции и подпрограммы — эти термины даже не существуют в спецификации этого языка. В VB единственное реальное различие между подпрограммой и функцией состоит в том, что подпрограмма никогда не возвращает значение. В C#, если метод не должен возвращать значение, то это объявляется как возвращение void (как выше проиллюстрировал метод OnClickShowResults()).

Синтаксис объявления метода аналогичен в обоих языках, по крайней мере в том, что параметры следуют за именем метода в скобках. Отметим, однако, что в то время как в VB подпрограмма объявляется с помощью ключевого слова Sub, в версии C# соответствующего слова не существует. В C# возвращаемого типа (void в данном случае), за которым следует имя метода и открывающая скобка, будет достаточно для сообщения компилятору, что объявлен метод, так как ни одна другая конструкция в C# не имеет подобного синтаксиса (массивы в C# помечаются квадратными, а не круглыми скобками, поэтому нет риска смешения с ними).

Подобно Sub в VB, приведенному выше объявлению метода в C# предшествует ключевое слово private. Оно имеет примерно то же значение, что и в VB — не позволяет внешнему коду видеть метод. Что точно понимается под "внешним кодом", будет рассмотрено позже.

Необходимо отметить еще два различия в объявлении метода: версия C# получает два параметра и имеет имя, отличное от обработчика событий в VB.

Сначала рассмотрим различие в имени. Имя обработчика событий в VB задается как VB IDE. VB знает, что Sub является обработчиком событий для нажатия кнопки, потому что используется имя cmdShowResults_Click. Если переименовать подпрограмму, то она не будет вызываться при нажатии кнопки. Однако C# не использует имя таким образом. В C#, как скоро будет показано, существует некий код, который сообщает компилятору, какой именно метод является обработчиком событий. Это означает, что обработчику можно задать любое имя. Однако что-нибудь начинающееся с On для обработчика событий является традиционным, в C# обычная практика именования методов (и в связи с этим большинства других элементов) с помощью так называемой системы имен в стиле Pascal которая означает, что слова соединяются вместе, а первая буква слова делается заглавной. Использование подчеркиваний в именах C# не рекомендуется, и имя здесь выбрано в соответствии с этими рекомендациями: OnClickShowResults().

Теперь о параметрах. В этом приложении не будут подробно рассматриваться параметры, но отметим просто, что все обработчики событий в C# должны иметь два параметра, аналогичные данным, и эти параметры могут предоставить некоторую дополнительную полезную информацию о событии (например, для события перемещения мыши они могут сообщать о положении указателя мыши).

Переменные

Пример SquareRoot показывает достаточно много различий между объявлениями переменных в C# и VB. В версии VB объявляется число с плавающей точкой и задается его значение таким образом:

Dim NumberInput As Single

NumberInput = CSng(Me.txtNumber.Text)

Версия C# выглядит следующим образом:

float NumberInput = float.Parse(this.txtNumber.Text);

Как и можно было ожидать, типы данных в C# не совсем такие, как в VB. float является эквивалентом в C# для Single. Наверно проще понять, что происходит, если разделить версию C# на две строки. Следующий код C# имеет точно такой же результат, что и строка выше:

float NumberInput;

NumberInput = float.Parse(this.txtNumber.Text);

Теперь можно сравнить объявление и инициализацию переменной по отдельности.

Объявления

Очевидное синтаксическое различие между C# и VB в объявлении переменных состоит в том, что в C# тип данных предшествует, а не следует за именем переменной без использования других ключевых слов. Это дает объявлениям C# более компактный формат, чем их аналогам в VB.

Отметим, что идея объявления, состоящего только из типа, за которым следует имя, используется также и в других местах. Посмотрим снова на объявление метода в C#:

private void OnClickShowResults(object sender, System.EventArgs e);

Тип (void) предшествует имени метода, не используя никаких других ключевых слов для указания того, что объявляется — это очевидно из контекста. То же самое справедливо для параметров. Типами параметров являются object и System.EventArgs. Тип object в C#, кстати, играет роль, аналогичную Object в VB,— он указывает то, для чего тип данных не был определен. Однако object в C# значительно более мощный, чем Object в VB, и в C# object заменяет тип данных Variant из VB. object мы рассмотрим позднее. System.EventArgs не будет рассматриваться подробно в этом приложении. Это базовый класс .NET и он не имеет аналога в VB.

В случае переменных синтаксис объявления, использованный в C#, позволяет комбинировать объявление с заданием начального значения переменной. В этом примере NumberInput инициализируется достаточно сложным выражением, которое скоро будет рассмотрено подробнее. Но сначала два простых примера:

int x = 10; // int аналогично Long в VB

string Message = "Hello World"; // string аналогично String в VB

Необходимо также отметить некоторые моменты, связанные с переменными.

Никаких суффиксов в C#

VB позволяет присоединять суффиксы к переменным, чтобы указать их тип данных: $ для String, % для Int, & для Long.

Dim Message$ ' будет string

Такой синтаксис не поддерживается в C#. Имена переменных могут содержать только буквы, цифры и символ подчеркивания, и необходимо всегда явно указывать тип данных.

Никаких значений по умолчанию для локальных переменных

В примере кода VB переменной NumberInput по умолчанию будет присвоено значение 0 после ее объявления. Это на самом деле ненужная фата процессорного времени, так как этой переменной немедленно в следующей инструкции присваивается новое значение. C# немного больше знает о производительности и не беспокоится о задании каких-либо значений по умолчанию для локальных переменных при их объявлении. Вместо этого он требует, чтобы такие переменные всегда инициализировались в коде программы до их использования. Компилятор C# будет инициировать ошибку компиляции, если попытаться прочитать значение локальной переменной прежде, чем она будет задана.

Присваивание значений переменным

Присваивание значений переменным в C# делается с помощью такого же синтаксиса, как и в VB. После имени переменной помещается знак =, за которым следует присваиваемое значение. Однако необходимо отметить, что это единый синтаксис, принятый в C#. В некоторых случаях в VB используется Let, в то время как для объектов в VB всегда используется ключевое слово Set:

Set MyListBox = new ListBox;

C# не использует отдельный синтаксис для присваивания объектных ссылок. Эквивалент в C# для вышеприведенного будет следующим:

MyListBox = new ListBox();

Помните, что в C# значения переменным всегда присваиваются с помощью синтаксиса <ИмяПеременной>=<Выражение>.

Классы

Теперь рассмотрим, что происходит в выражении, используемом для инициализации переменной NumberInput в примере SquareRoot. В C# и VB делается практически одно и то же: извлекается текст из текстового поля txtNumber. Но синтаксис этого выглядит по-разному в двух этих языках:

NumberInput = CSng(Me.txtNumber.Text)

и

float NumberInput = float.Parse(this.txtNumber.Text);

Получение значения из текстового поля достаточно похоже в обоих случаях. Единственное различие для этой части процесса является чисто синтаксическим — VB использует ключевое слово Me, в то время как C# применяет ключевое слово this, которое имеет точно такое же значение (фактически, в C# можно его при желании опустить, так же как можно опустить Me в VB). В C# можно было в равной степени написать:

float NumberInput = float.Parse(txtNumber.Text);

Более интересной частью является то, как строка, извлеченная из текстового поля, преобразуется во float (или single), потому что это иллюстрирует фундаментальное свойство языка C#, о котором кратко упоминалось ранее:

В C# все является частью класса.

В VB для преобразования используется функция CSng. Однако C# не имеет функций в том виде, как в VB. C# является полностью объектно-ориентированным и разрешает объявлять только те методы, которые являются частью класса.

В C# преобразование из строки в число с плавающей точкой выполняется методом Parse(). Однако, так как Parse() является частью класса, ему должно предшествовать имя класса. Класс, на котором необходимо вызвать метод Parse(), будет классом float. До сих пор float интерпретировался просто как эквивалент C# для Single из VB. Но на самом деле он также является классом. В C# все типы данных тоже являются классами, и значит, такие вещи как int, float и string имеют методы и свойства, которые можно вызывать (хотя необходимо отметить, что int и float являются специальными типами класса, известного в C# как структуры. Различие для этого кода не важно, но оно будет объяснено позже.)

Если внимательно посмотреть на приведенный выше код, можно отметить незначительную проблему с аналогией, касающейся модулей класса VB. В методы вызываются определением имени переменной, а не имени модуля класса, но Parse вызван с помощью определения имени класса float, а не имени переменной. Parse() в действительности является специальным типом метода, называемого статическим (static) методом. Статический метод можно вызывать, не создавая экземпляр класса. Следовательно здесь определяется имя класса float, а не имя переменной. static имеет в C# значение, отличное от того, которое он имеет в VB. В C# нет эквивалента статическим переменным VB — в них нет необходимости в объектах C#, так как с этой целью будут использоваться поля C#.

Инструкции If

Мы переходим к основной части обработчика событий — инструкции if. Вспомните, что версия VB выглядит следующим образом:

If (NumberInput < 0) Then

Me.txtSign.Texgt = "Negative"

Me.txtResult.Text = CStr(Sqr(-NumberInput)) & " i"

ElseIf (NumberInput = 0) Then

txtSign.Text = "Zero"

txtResult.Text = "0"

Else

Me.txtSign.Text = "Positive"

Me.txtResult.Text = CStr(Sqr(NumberInput))

EndIf

в то время как версия C# записывается так:

if (NumberInput < 0) {

this.txtSign.Text = "Negative";

this.txtResult.Text = Math.Sqrt(-NumberInput).ToString() + " i";

} else if (NumberInput == 0) {

txtSign.Text = "Zero";

txtResult.Text = "0";

} else {

this.txtSign.Text = "Positive";

this.txtResult.Text = Math.Sqrt(NumberInput).ToString();

}

Фактически наибольшее синтаксическое различие здесь уже было объяснено: каждая часть инструкции в C# должна быть одиночной инструкцией, следовательно, если необходимо условно выполнить более одной инструкции, надо объединить их в одну блочную инструкцию. В C#, если существует только одна инструкция для условного выполнения, не нужно формировать блочную инструкцию. Например, если пропустить задание текста в текстовом поле txtSign в приведенном выше коде, то можно написать:

if (NumberInput < 0) this.txtResult.Text = Math.Sqrt(-NumberInput) + " i";

else if (NumberInput == 0) txtSign.Text = "Zero";

else this.txtResult.Text = Math.Sqrt(NumberInput).ToString();

Существуют и другие различия в синтаксисе, которые необходимо отметить. В C# скобки вокруг условия, которое проверяется в инструкции if, является обязательным. В VB можно написать:

If NumberInput < 0 Then

Попытка выполнить то же самое в C# немедленно приведет к ошибке компиляции. В целом C# значительно более точен в отношении ожидаемого синтаксиса, чем VB. Отметим также, что при проверке равенства нулю NumberInput для сравнения используются два последовательных знака равенства:

else if (NumberInput == 0)

В VB символ = применяется в двух назначениях: для присваивания значений переменным и для сравнения значений. C# формально распознает это как операции двух различных типов и поэтому использует два различных символа: = для присваивания и == для сравнения.

Существует еще одно важное различие, которое надо учитывать, так как оно может легко привести к ошибкам при переходе от VB к C#:

else if состоит в C# из двух слов, а в VB используется одно слово ElseIf

Вычисление квадратного корня: еще один метод класса

В соответствии со сделанным ранее замечанием о том, что все в C# является членом класса, будет неудивительно узнать, что эквивалент в C# функции Sqr из VB, которая вычисляет квадратный корень, также является методом, являющимся членом класса. В данном случае это метод Sqrt(), который представляют статический член другого базового класса .NET с именем System.Math, сокращаемый в коде просто до Math.

Можно также заметить, что в этом примере в условии с введенным числом, точно равным нулю, ключевое слово this в коде C# не определено:

txtSign.Text = "Zero";

txtResult.Text = "0";

и в соответствующем коде VB также не определяется явно Me. В C# по аналогии с VB, не требуется явно определять this (Me), если только по какой-то причине контекст окажется не ясным. Здесь это делается только для иллюстрации.

Строки

При выводе квадратного корня отрицательного числа выполняется небольшая работа со строками:

this.txtResult.Text = Math.Sqrt(-NumberInput).ToString() + " i";

Этот код покалывает, что в C# конкатенация строк делается с помощью символа +, а не &. Можно также заметить, что преобразование из float в String выполняется с помощью вызова метода на объекте float. Метод называется ToString(), и он не является статическим, поэтому вызывается с помощью того же синтаксиса, что и в VB при вызове методов на объектах, способом задания перед именем метода имени переменной, представляющей объект, с точкой. В отношении C# необходимо помнить одну полезную вещь — каждый объект (и следовательно, каждая переменная) реализует метод ToString().

Дополнительный код в C#

Мы завершили сравнение процедур обработки событий в C# и VB. В процессе обсуждения мы много узнали о синтаксических различиях между этими языками. Фактически была показана большая часть базового синтаксиса C#. Мы также впервые столкнулись с тем фактом, что все в C# является классом. Однако, если загрузить код нашего примера с web-сайта издательства Wrox, и просмотреть его, то почти наверняка можно будет заметить, что мы тщательно избегали какого-либо обсуждения наиболее очевидного различия между двумя примерами: в примере на C# в действительности код намного длиннее и включает не только обработчик событий. Для версии VB примера SquareRoot код обработчика событий, который здесь представлен, представляет весь исходный код этого проекта. Напротив, в версии C# этот обработчик событий является только одним методом в огромном файле исходного кода.

Причина, по которой код в проекте C# такой большой, связана с тем, что Visual Basic IDE большая часть того, что делается в программе, исходит от программиста. В Visual Basic требовалось написать только обработчик событий, но фактически выполняется значительно больше: запускается пример, выводится форма на экране, посылается информация Windows, зависящая от того, что желательно делать с событиями, и по окончании пример завершается. В Visual Basic программист не имеет доступа к коду, который делает все. C#, напротив, использует совершенно другую философию, и оставляет весь этот код открытым. Это может сделать внешний вид исходного кода более сложным. Но имеется одно преимущество: если код доступен, то его можно редактировать и это обеспечивает значительно большую гибкость в решении того, как должно себя вести приложение.

Фактически Visual Basic настолько успешно скрывает почти все, что происходит в программе, что очень легко стать профессиональным программистом и создавать достаточно сложные приложения, не имея на самом деле никакого представления о полной структуре компьютерной программы. В следующем разделе будет рассмотрено, что же происходит в любой такой программе, и таким образом мы будем готовы взглянуть на весь дополнительный код, который содержится в версии C# программы SquareRoot.

Что происходит при выполнении программы

Любая программа содержит точную последовательность выполнения. Когда приложение запускается, будет существовать определенное место в исполнимом коде, с которого, как знает компьютер, он должен начать выполнение кода, другими словами, инструкция, которая выполняется первой. Затем будет идти следующая инструкция, и следующая, и следующая и так далее. Некоторые из этих команд прикажут компьютеру перепрыгнуть к другой инструкции, возможно в зависимости от значений, которые содержатся в некоторых переменных. Очень часто компьютер будет перепрыгивать назад и выполнять ту же самую инструкцию снова. Однако всегда существует эта непрерывная последовательность выполнения следующей инструкции, пока компьютер не встретит команду, которая прикажет ему прекратить выполнение кода. Такая линейная последовательность справедлива для любой программы. Некоторые программы могут быть мультипоточными, и в этом случае существует несколько последовательностей выполнения (потоков выполнения), но каждый поток по-прежнему будет следовать от начальной инструкции до остановки программы.

Конечно, эта последовательность не та, которую можно видеть при написании исполнимой программы VB. В VB6 по сути пишется набор обработчиков событий — набор подпрограмм, каждая из которых вызывается, когда пользователь что-то сделает. Не существует единственного начала программы, хотя обработчик события Form_Load близко подходит к этому по своей концепции. Даже в этом случае Form_Load является на самом деле только еще одним обработчиком для события, которое возникает, когда загружается форма, и значит, оно будет первым выполняющимся событием. Аналогично, если вместо исполнимого кода пишется элемент управления или объект класса, то нет начальной точки. Просто задается класс и к нему добавляется множество методов и свойств. Каждый метод или свойство будет выполняться, если или когда код клиента его вызывает.

В действительности приведенный выше абзац не совсем справедлив. В VB существует процедура Sub Main(), которая действует как точка входа программы, но она не часто используется. Поскольку здесь сравнивается типичная программа C# с типичной программой VB, то точка зрения, констатирующая, что программы VB в действительности показывают код только для событие в общем оказывается правильной.

Чтобы увидеть, как можно связать две идеи программирования, давайте выясним, что реально происходит, когда выполняется любое приложение Visual Basic или любое приложение GUI Windows, не важно на каком языке оно написано. Это более ограниченное рассмотрение, чем в случае приложении, которые упоминались перед этим, так как теперь мы сосредоточимся только на приложениях Windows API (другими словами, нет консолей, служб и т.д.).

Как обычно, выполнение начинается в некоторой вполне определенной точке. Команды будут вовлекать в работу некоторые окна и элементы управления, а также вывод этих элементов управления на экран. В этом месте программа делает затем что-то, что называют входом в цикл сообщений. На самом деле программа засыпает и приказывает Windows разбудить себя, когда произойдет что-то интересное, о чем ей необходимо знать. Эти "интересные" вещи являются событиями, для которых необходимо написать обработчики событий, а также существует достаточно много событий, для которых не требуется писать свои собственные обработчики событий, так как если обработчик для определенного события написан не будет, то VB IDE спокойно представляет готовый по умолчанию. Хорошим примером этого являются обработчики, которые имеют дело с изменением размера формы. Исходный код для него никогда не показывается в VB, но приложение VB может правильно отреагировать, когда пользователь попытается изменить размер, поскольку VB IDE незаметно добавляет в проект обработчики событий, которые правильно управляют этой ситуацией.

Когда происходит событие, Windows пробуждает приложение и вызывает соответствующим обработчик событий, именно в этот момент может начать выполняться код программиста. Когда процедура обработчика событий заканчивается, приложение снова переведет себя в спящий режим, приказывая Windows разбудить себя, когда произойдет другое интересное событие. Наконец, предполагая, что ничего не произошло разрушительного и неверного, в некоторый момент Windows разбудит приложение и проинформирует его, что необходимо прекратить работу. В этот момент приложение предпримет все необходимые действия — например, выведет окно сообщения, спрашивающего у пользователя, не хочет ли он сохранить файл и затем спокойно завершится. Снова большая часть кода, который это делает, добавляется в проект неявно VB IDE, и программист никогда его не видит.

Поток выполнения в типичном приложении GUI Windows выглядит примерно следующим образом:

На этой диаграмме рамка с пунктирной границей указывает ту часть исполнимого кода, к которой VB IDE предоставляет программисту доступ и для которого можно писать исходный код,— это несколько обработчиков событий. Остальная часть кода недоступна для программиста, хотя его можно в некоторой степени определить, выбирая тип приложения при запросе к VB на создание проекта. Для нового проекта в VB появляется диалоговое окно, запрашивающее тип приложения, которое требуется создать,— стандартный EXE, ActiveX EXE, ActiveX DLL и т.д. После выбора VB IDE использует его для генерации всего необходимого кода в той части программы, которая находится вне пунктирной рамки на приведенной выше диаграмме. Диаграмма показывает ситуацию, когда выбрано создание проекта стандартного EXE, отличающегося от других типов проектов (например, ActiveX DLL вообще не имеет цикла сообщений, но вместо этого зависит от клиентов в отношении вызова методов), и при этом дается примерное представление о том, что происходит.

Ранее было сказано, что в C# программист получает доступ ко всему коду, теперь необходимо это уточнить: все мельчайшие подробности относительно того, что происходит в цикле сообщений, надежно скрыты внутри различных DLL, которые написала компания Microsoft, но можно видеть методы верхнего уровня, вызывающие различные элементы обработки. Допустим, имеются доступы к коду, который начинает выполнение всей программы, к вызову библиотечного метода, который заставляет программу войти в цикл сообщений и переводит ее в спящее состояние и т.д. Имеется также доступ к исходному коду, создающему экземпляры всех различных элементов управления, которые помещаются на форме, делая эти экземпляры видимыми и определяя их начальные положения, размеры и все остальное. Необходимо отметить еще и тот момент, что не требуется писать никакой подобный код самостоятельно. При использовании Visual Studio.NET для создания проекта C# по-прежнему появляется диалоговое окно, спрашивающее, какой тип проекта необходимо создать, и Visual Studio.NET будет по-прежнему готовить весь базовый код. Различие состоит в том, что Visual Studio.NET записывает этот базовый код, как исходный на C#, который затем можно непосредственно редактировать.

Все это приводит к тому, что исходный код получается более длинным и более сложным. Однако огромное его преимущество заключается в том, что он обеспечивает значительно большую гибкость в том, что делает программа и как она себя ведет. А значит, можно написать намного больше типов проектов в C#. В то время как в Visual Basic возможно написание только различных видов форм и компонентов COM, в C# вы в праве написать любую из различных типов программ, которые выполняются под Windows. Это включает, например, консольные приложения (командной строки) и страницы ASP.NET (наследник ASP), что нельзя написать в VB6 (можно использовать VBScript для страниц ASP). В этом приложении, однако, мы сосредоточимся на классических приложениях GUI для Windows.

Код C# для оставшейся части программы

В этом разделе будет рассмотрена оставшаяся часть кода для примера SquareSample, в результате чего мы узнаем немного больше о классах в C#.

Пример SquareRoot на C# был создан в Visual Studio.NET, а пример на VB — в IDE VB6. Однако представленный здесь код является не совсем тем, который сгенерировала Visual Studio.NET. Помимо добавления обработчика событий, сделаны и другие изменения в коде, чтобы лучше проиллюстрировать принципы программирования в C#. Но несмотря ни на что, он все-таки даст хорошее представление о той работе, которую делает Visual Studio.NET, когда создает проект.

Весь текст исходного кода достаточно длинный. Он представлен здесь для полноты, но лучше сразу перейти к последующим объяснениям и обращаться к исходному коду по мере необходимости.

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

namespace Wrox.ProfessionalCSharp.AppendixC.SquareRootSample {

/// <summary>

/// Form, которая формирует основное окно приложения:

/// </summary>

public class SquareRootForm : System.Windows.Forms.Form {

private System.Windows.Forms.TextBox txtNumber;

private System.Windows.Forms.TextBox txtSign;

private System.Windows.Forms.TextBox txtResult;

private System.Windows.Forms.Button cmdShowResults;

private System.Windows.Forms.Label label1;

private System.Windows.Forms.Label label2;

private System.Windows Forms.Label label3;

private System.Windows.Forms.Label label4;

/// <summary>

/// Необходимые для designer переменные.

/// </summary>

private System.ComponentModel.Container components;

public SquareRootForm() {

InitializeComponent();

}

public override void Dispose() {

base.Dispose();

if(components != null) components.Dispose();

}

#region Windows Form Designer generated code

/// <summary>

/// Требуемый для поддержки Designer метод - не изменять

/// содержимое этого метода с помощью редактора кода.

/// </summary>

private void InitializeComponent() {

this.txtNumber = new System.Windows.Forms.TextBox();

this.txtSign = new System.Windows.Forms.TextBox();

this.cmdShowResults = new System.Windows.Forms.Button();

this.label3 = new System.Windows.Forms.Label();

this.label4 = new System.Windows.Forms.Label();

this.label1 = new System.Windows.Forms.Label();

this.label2 = new System.Windows.Forms.Label();

this.txtResult = new System.Windows.Forms.TextBox();

this.SuspendLayout();

//

// txtNumber

//

this.txtNumber.Location = new System.Drawing.Point(160, 24);

this.txtNumber.Name = "txtNumber";

this.txtNumber.TabIndex = 0;

this.txtNumber.Text = "";

//

// txtSign

//

this.txtSign.Enabled = false;

this.txtSign.Location = new System.Drawing.Point(160, 136);

this.txtSign.Name = "txtSign";

this.tхtSign.TabIndех = 1;

this.txtSign.Text = "";

//

// cmdShowResults

//

this.cmdShowResults.Location = new System.Drawing.Point(24, 96);

this.cmdShowResults.Name = "cmdShowResults";

this.cmdShowResults.Size = new System.Drawing.Size(88, 23);

this.cmdShowResults.TabIndex = 3;

this.cmdShowResults.Text = "Show Results";

this.cmdShowResults.Click +=

new System.EventHandler(this.OnClickShowResults);

//

// label3

//

this.label3.Location = new System.Drawing.Point(72, 24);

this.label3.Name = "label3";

this.label3.Size = new System.Drawing.Size(80, 23);

this.label3.TabIndex = 6;

this.label3.Text = "Input a number";

//

// label4

//

this.label4.Location = new System.Drawing.Point(80, 184);

this.label4.Name = "label4";

this.label4.Size = new System.Drawing.Size(80, 16);

this.label4.TabIndex = 7;

this.label4.Text = "Square root is";

//

// label1

//

this.label1.Location = new System.Drawing.Point(112, 136);

this.label1.Name = "label1";

this.label1.Size = new System.Drawing.Size(40, 23);

this.label1.TabIndex = 4;

this.label1.Text = "Sign is";

//

// label2

//

this.label2.Location = new System.Drawing.Point(48, 184);

this.label2.Name = "label2";

this.label2.Size = new System.Drawing.Size(8, 8);

this.label2.TabIndex = 5;

//

// txtResult

//

this.txtResult.Enabled = false;

this.txtResult.Location = new System.Drawing.Point(160, 184);

this.txtResult.Name = "txtResult";

this.txtResult.TabIndex = 2;

this.txtResult.Text = "";

//

// Form1

//

this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);

this.ClientSize = new System.Drawing.Size(292, 269);

this.Controls.AddRange(new System.Windows.Forms.Control[] {

this.label4, this.label3, this.label2, this.label1,

this.cmdShowResults, this.txtResult, this.txtSign, this.txtNumber

});

this.Name = "Form1";

this.Text = "Square Root C# Sample";

this.ResumeLayout(false);

}

#endregion

/// <summary>

/// Обработчик событий для нажатия пользователем кнопки Show Results

/// Выводит квадратный корень и знак числа

/// </summary>

/// <param name="sender"></param>

/// <param name="e"></param>

private void OnClickShowResults(object sender, System.EventArgs e) {

float NumberInput = float.Parse(this.txtNumber.Text);

if (NumberInput < 0) {

this.txtSign.Text = "Negative";

this.txtResult.Text = Math.Sqrt(-NumberInput) + " i";

} else if (NumberInput == 0) {

txtSign.Text = "Zero";

txtResult.Text = "0";

} else {

this.txtSign.Text = "Positive";

this.txtResult.Text = Math.Sqrt(NumberInput).ToString();

}

}

}

class MainEntryClass {

/// <summary>

/// Основная точка входа приложения.

/// </summary>

[STAThread]

static void Main() {

SquareRootForm TheMainForm = new SquareRootForm();

Application.Run(TheMainForm);

}

}

}

Пространства имен

Основная часть исходного кода SquareRoot на C# начинается с объявлений пространств имен и класса:

namespace Wrox.ProfessionalCSharp.AppendixC.SquareRootForm {

public class SquareRootForm : System.Windows.Forms.Form {

Класс SquareRootForm будет содержать почти весь код — все методы и т.д. с небольшим объемом кода, находящимся в классе с именем MainEntryClass. Помните что легче всего здесь представлять класс как объект класса VB, хотя есть одно различие, состоящее в том, что реально виден исходный код, который начинается с объявления класса. В VB среда разработки — это просто отдельное окно, содержащее код класса.

Пространство имен не имеет аналогии в VB и проще всего представить его как способ организации имен классов таким образом, как файловая система организует имена файлов. Например, почти наверняка на жестком диске имеется большое количество файлов, которые имеют имя ReadMe.txt. Если бы это имя было единственной информацией о каждом файле, то невозможно было бы различить все эти файлы. Но есть полные имена доступа, например, C:\Program Files\ReadMe.txt и G:\Program Files\HTML Help Workshop\ReadMe.txt.

Пространства имен работают так же, но без дополнительных расходов, связанных с созданием реальной файловой системы — они являются по сути не более чем метками. Формально не требуется ничего делать, чтобы создать пространство имен, кроме просто объявления его в коде таким способом, как было сделано в примере выше. Код, представленный в нем, означает, что полное имя класса, который был определен, будет не SquareRootForm, a Wrox.ProfessionalCSharp.AppendixC.SquareRootForm. Крайне маловероятно, что кто-то будет записывать класс с этим полным именем. С другой стороны, если бы не было пространства имен, то существовал бы большой риск путаницы, так как кто-нибудь еще мог бы написать класс с именем SquareRootForm.

Исключение конфликтов такого рода важно в C#, так как рабочая среда .NET использует только эти имена для идентификации классов, в то время как элементы управления ActiveX, созданные VB, применяют для ухода от конфликтов имен сложный механизм, включающий GUID. Компания Microsoft предпочла более простую концепцию пространств имен в связи с опасениями, что некоторые сложности COM, такие как GUID, сделают неоправданно трудным для разработчиков создание хороших приложений Windows.

Хотя в C# пространства имен и не являются строго обязательными, настоятельно рекомендуется все классы помещать в пространство имен, чтобы предотвратить любые возможные конфликты имен с другим программным обеспечением. Фактически крайне редко можно увидеть код C#, который не начинается с объявления пространства имен.

Пространства имен могут быть вложенными. Например, приведенный выше код пространства имен:

namespace Wrox.ProfessionalCSharp.AppendixC.SquareRootSample {

public class SquareRootForm : System.Windows.Forms.Form {

// и т.д.

}

}

можно было бы записать следующим образом:

namespace Wrox {

namespace ProfessionalCSharp {

namespace AppendixC {

namespace SquareRootSample {

public class SquareRootForm : System.Windows.Forms.Form {

// и т.д.

}

}

}

}

}

В этом коде добавлены закрывающие фигурные скобки, просто чтобы подчеркнуть, что они всегда должны соответствовать открывающим. Фигурные скобки используются для отметки границ пространств имен и классов так же, как они используются для отметки границ методов и составных инструкций.

Инструкция using

Конечная часть приведенного выше кода, который начинает проект SquareRoot, состоит из инструкций using:

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

namespace Wrox.ProfessionalCSharp.AppendixC.SquareRootSample {

Эти инструкции using присутствуют здесь, чтобы упростить код. Полные имена классов, включающие имена пространств имен, будут длинными. Например, позже в этом коде определяется пара текстовых полей. Текстовое поле представляется классом System.Windows.Forms.TextBox. Если писать это в коде каждый раз при ссылке на Text Box, код будет выглядеть очень загроможденным. Вместо этого инструкция using System.Windows.Forms; дает задание компилятору найти в этом пространстве имен все классы, которые отсутствуют в текущем пространстве имен и для которых не определено пространство имен. Теперь можно просто писать TextBox везде, где необходимо сослаться на этот класс. Обычно любая программа на C# начинается с ряда инструкций using, вводящих все пространства имен, которые будут использоваться в множество пространств имен, просматриваемых компилятором. Пространства имен, определенные в приведенном выше коде охватывают различные части библиотеки базовых классов .NET, и поэтому позволяют, что очень удобно, использовать различные базовые классы .NET.

Определение класса: наследование

Теперь мы переходим к определению класса SquareRootForm. Само определение достаточно простое:

public class SquareRootForm : System.Windows.Forms.Form {

Ключевое слово class сообщает компилятору, что будет определен класс. Интерес в данном случае представляет двоеточие после имени класса, за которым следует другое имя — Form. Это момент, где мы вводим упоминавшуюся ранее важную концепцию, которую необходимо знать, чтобы понимать программирование на C#,— наследование

Приведенный выше синтаксис сообщает компилятору, что класс SquareRootForm наследуется из класса Form (в действительности из Windows.Forms.Form). Это означает, что класс получает не только все методы, свойства и т.д., которые мы определяем, он получает все, что было в классе Form. Form является очень мощным базовым классом .NET, который предоставляет все свойства базовой формы. Он содержит методы, позволяющие форме выводиться, и большое количество свойств, включая Height, Width, Desktop Location, BackColor (фоновый цвет формы), которые управляют внешним видом формы на экране. Наследуя от этого класса, наш собственный класс сразу получает все эти свойства и является поэтому полноценной формой. Класс, от которого наследуют, называется базовым классом, а новый класс называют производным классом.

Если вы знакомы с интерфейсами, то наследование не должно быть для вас новым понятием, так как интерфейсы могут наследоваться друг из друга. Однако здесь имеется значительно более мощная конструкция, чем наследование интерфейсов. Когда интерфейс COM наследуется из другого интерфейса, он получает только имена и сигнатуры методов и свойства. Это, в конце концов, все, что содержит интерфейс. Однако класс содержит весь код, который реализует эти методы, и тому подобное также, как в VB делает объект класса. Это означает, что SquareRootForm получает все реализации из класса Form, а также имена методов. Этот вид наследования называется наследованием реализации, он не является новинкой в C#: это была фундаментальная концепция классического объектно-ориентированного программирования (OOP), которой пользовались в течение десятилетий. Программы C++, в частности, обычно работают на основе этой концепции, но она не поддерживается VB. (Наследование реализации имеет сходство с созданием подклассов.) При разработке программ на C# можно обнаружить, что вся архитектура типичной программы C# почти всегда основывается на наследовании реализации.

Но наследование реализации является еще более мощным средством. Как будет показано позже, когда класс наследуется из другого класса, он не обязан брать все реализации в базовом классе. При желании можно изменить реализации определенных методов и свойств с помощью технологии, называемой переопределением. Это означает, что можно создать класс, который очень похож на существующий, но имеет некоторые отличия в том. как и что он делает. Это существенно облегчает повторное использование кода, написанного другими людьми, сберегая тем самым время разработки. Также важно понять, что не требуется доступ к исходному коду базового класса, чтобы наследовать из него. По очевидным коммерческим соображениям компания Microsoft сохраняет исходный код класса Form для себя. Тот факт, что компилированная библиотека доступна в форме сборки, является достаточным, чтобы можно было наследовать от этого класса, используя требуемые методы и переопределяя методы, которые не нужны.

Точка входа в программу

Основной точкой входа в программу является функция Main():

class MainEntryClass {

/// <summary>

/// Основная точка входа приложения.

/// </summary>

[STAThread]

static void Main() {

SquareRootForm TheMainForm = new SquareRootForm();

Application.Run(TheMainForm);

}

}

Это не очевидная точка входа в программу, но это — она. Правило в C# говорит, что выполнение программы начинается с метода с именем Main(). Этот метод должен быть определен как статический в том же классе. Обычно должен быть только один метод во всех классах в исходном коде, который отвечает этому описанию в программе, иначе компилятор не будет знать, какой из них выбрать. Main() здесь определен без параметров и как возвращающий void (другими словами, не возвращающий ничего). Это не единственная возможная сигнатура этого метода, но это обычная сигнатура для приложения Windows (приложения командной строки получают параметры — это любые аргументы, задаваемые в командной строке).

Как упоминалось раже, код VB может иметь метод Main(), но он редко используется и не является обязательным. В C# метод Main() должен присутствовать как основная точка входа в программу.

Так как метод Main() должен быть в классе, то здесь присутствует класс с именем MainEntryClass. В этом классе нет ничего другого, но это не обязательно должно быть так, вполне допустимо для класса, в котором определена точка входа в программу, содержать и другие методы. Тот факт, что метол Main() — статический метод, является важным. Мы говорили раньше, что статические методы являются специальными методами, которые могут выполняться без реального создания в начале программы объекта класса. Так как при выполнении прежде всего вызывается метод Main(), то в этот момент не существует никаких экземпляров никаких классов — еще не выполнился никакой код для их создания. Вот почему точка входа должна быть статической.

Помимо ключевого слова static, определение Main() выглядит, как и предыдущие рассмотренные определения методов. Однако перед ним стоит в квадратных скобках слово [STAThread], [STAThread] является примером атрибута — еще одной концепции, которая не имеет аналогов в исходном коде VB.

Атрибут является конструкцией, предоставляющей дополнительную информацию компилятору о некоторых элементах кода, и всегда имеющей форму слова (возможно также с некоторыми параметрами, хотя не в данном случае) в квадратных скобках сразу перед элементом, к которому он применяется. Этот конкретный атрибут сообщает компилятору о модели потоков выполнения, в которой должен выполняться код. Детали моделей потоков выполнения здесь рассматриваться не будут, но можно сказать, что запись [STAThread] в исходном коде C# имеет эффект, аналогичный выбору модели потоков выполнения в Project Properties в VB IDE, хотя в VB это можно делать только для проектов ActiveX DLL и ActiveX Control. Отметим также, что эта аналогия только приблизительная, так как атрибут C# выбирает модель потоков выполнения .NET, а не модель потоков COM.

Создание экземпляров классов

Давайте теперь рассмотрим код внутри метода Main(). Прежде всего необходимо создать форму — другими словами, экземпляр объекта SquareRootForm. Это делает первая строка кода:

SquareRootForm TheMainForm = new SquareRootForm();

Очевидно, что этот код нельзя сравнить с соответствующим кодом VB, потому что такие команды VB недоступны как исходный код, но можно сделать сравнение, если представить, что в некотором коде VB необходимо создать диалоговое окно. В VB это будет выглядеть примерно следующим образом:

Dim SomeDialog As MyDialogClass

Set SomeDialog = New MyDialogClass

В этом коде VB сначала объявляется переменная, которая является объектной ссылкой — SomeDialog будет ссылаться на экземпляр MyDialogClass. Затем реально создается экземпляр объекта с помощью ключевого слова New из VB и присваивается переменной ссылка на этот объект.

Это совпадает с тем, что происходит в коде C#: объявляется переменная с именем TheMainForm, которая является ссылкой на объект SquareRootForm, затем используется ключевое слово C# new для создания экземпляра SquareRootForm, и после этого мы задаем переменной ссылку на этот объект. Основное синтаксическое различие состоит в том, что C# позволяет объединить обе операции в одной инструкции таким же образом, как ранее сразу объявлялась и инициализировалась переменная NumberInput. Можно также заметить скобки после выражения new — это требование C#. При создании объектов всегда необходимо записывать эти скобки, потому что C# интерпретирует создание объекта несколько похоже на вызов метода, так что даже можно иногда передавать параметры в вызов new, чтобы указать, как желательно инициализировать новый объект. В данном случае параметры не передаются, но скобки все равно должны использоваться.

Классы С#

До сих пор говорилось, что классы C# похожи на модули классов в VB. Мы уже видели одно различие, заключающееся в том, что классы C# допускают статические методы. Приведенный выше код метода Main() подчеркивает теперь еще одно различие: если делается что-то подобное в VB, то необходимо также задать для созданного объекта значение Nothing, когда работа с ним будет закончена. Однако ничего подобного не появляется в коде C#, так как в C# этого делать вовсе не нужно.

Причина этого различия состоит в том, что классы C# являются более эффективными и динамичными, чем их соответствующие аналоги в VB. Объекты классов VB являются на самом деле объектами COM, то есть каждый из них включает некоторый сложный код, который проверяет, сколько ссылок на объект поддерживается, поэтому каждый объект может разрушить себя, когда обнаружит, что он больше не нужен. В VB, если не задать объектную ссылку Nothing после завершения работы с объектом, это будет рассматриваться как плохая практика программирования, так как это означает, что объект не знает, что он больше не нужен, поэтому он может висеть в памяти возможно до окончания всего процесса.

Однако по соображениям производительности объекты C# не выполняют проверку такого рода. Вместо этого C# использует механизм, называемый сборкой мусора. При этом вместо того, чтобы каждый объект проверял, что он должен все еще существовать, среда выполнения .NET время от времени передает управление так называемому сборщику мусора. Сборщик мусора исследует состояние памяти, используя очень эффективный алгоритм для идентификации тех объектов, которые больше не нужны коду, и удаляя их. При наличии такого механизма неважно сбрасываются ли ссылки, когда работа с ними закончена, обычно достаточно просто подождать, пока переменная выйдет из области видимости.

Но если желательно задать ссылочную переменную, которая ни на что не указывает, то соответствующим ключевым словом C# является null, которое означает то же самое, что и Nothing в VB. Следовательно, там, где в VB было бы написано:

Set SomeDialog = Nothing;

в C# будет написано:

TheMainForm = null;

Заметим, что это само по себе делает не так уж много в C#, поскольку объект все равно не будет разрушен, пока не вызовется сборщик мусора.

Вход в цикл сообщений

Рассмотрим теперь конечную инструкцию в основном методе:

Application.Run(TheMainForm);

Эта инструкция запускает цикл сообщений. На самом деле здесь вызывается статический метод Run() класса System.Windows.Forms.Application. Этот метод обрабатывает цикл сообщений. Он переводит приложение (или, строго говоря, поток выполнения) в спящее состояние и просит Windows разбудить его, когда произойдет интересное событие. Метод Run() может получать один параметр, являющийся ссылкой на форму, которая будет обрабатывать все события. Run() заканчивается, когда произойдет и обработается событие, дающее указание форме завершить работу.

Когда метод Run() подходит к концу, то и метод Main() завершается. Так как этот метод был точкой входа в программу, то по его завершении выполнение всего процесса останавливается.

Один элемент синтаксиса в приведенных выше инструкциях, который может показаться удивительным, состоит в том, что при вызове метода Run() используются скобки, даже хотя никакое возвращаемое значение из этого метода не используется, и, следовательно, выполняется вызов, эквивалентный вызову подпрограммы в VB. VB в этом случае не требует скобок, но в C# существует правило, что при вызове метода всегда используются скобки.

При вызове любого метода в C# всегда используйте скобки, независимо от того, будет или нет использоваться возвращаемое значение.

Класс формы SquareRoot

Мы видели, как C# запускает цикл сообщений, но мы еще не изучили процесс вывода и создания самой формы. Мы также не определились с вопросом о вызове обработчиков событий. Упоминалось, что Windows вызывает такие обработчики событий, как метод OnClickButtonResults(). Но как Windows узнает, что нужно вызвать этот метод? Мы найдем ответы на все наши вопросы в определении класса SquareRootForm и в его базовом классе Form.

Отметим сначала, что класс SquareRootForm имеет достаточно много полей-членов. (Поле-член является выражением C# для переменной, которая определена как член класса. Можно представлять ее как переменную VB, которая имеет областью действия форму, или как переменную VB, которая определена в качестве члена модуля класса. Каждая такая переменная связана с определенным экземпляром класса — определенным объектом — и остается в области действия до тех пор, пока существует содержащий ее объект):

public class SquareRootForm : System.Windows.Forms.Form {

private System.Windows.Forms.TextBox txtNumber;

private System.Windows.Forms.TextBox txtSign;

private System.Windows.Forms.TextBox txtResult;

private System.Windows.Forms.Button cmdShowResults;

private System.Windows.Forms.Label label1;

private System.Windows.Forms.Label label2;

private System.Windows.Forms.Label label3;

private System.Windows.Forms.Label label4;

Каждое из этих полей соответствует одному из элементов управления. Можно легко увидеть три текстовых поля и кнопку. Имеются также четыре метки, соответствующие областям текста на форме. Мы не будем ничего делать с этими метками, поэтому не стоит беспокоиться и давать им какие-то более понятные пользователю имена.

Однако каждая из этих переменных является просто ссылкой на объект, поэтому тот факт, что эти переменные существуют, не означает существования никаких экземпляров этих объектов — экземпляры объектов должны быть созданы отдельно. Процесс создания экземпляров этих элементов управления осуществляется с помощью так называемого конструктора. Конструктор в C# является примерным аналогом таким подпрограммам, как Form_Load(), Form_Initialize(), Class_Load() и Class_Initialize(). Это специальный метод, который автоматически вызывается, когда создается экземпляр класса, и он содержит код, необходимый для инициализации экземпляра.

Конструктор в классе легко опознать, так как он всегда имеет такое же имя, как и сам класс. В данном случае мы ищем метод с именем SquareRootForm:

public SquareRootForm() {

InitializeComponent();

}

Отметим, что так как это — конструктор, а не метод, который можно вызывать, он не определяет никакого возвращаемого типа. Однако после его имени стоят скобки, как и у метода. Можно использовать эти скобки для определения параметров, которые будут передаваться в конструктор (вспомните, что при создании переменной можно передавать параметры в скобках после предложения new). Определение конструктора указывает, нужны ли какие-то параметры для создания экземпляра объекта. Однако здесь нет никаких параметров. Мы увидим конструкторы, которые получают параметры, в примере Employee, рассматриваемом дальше в этом приложении.

В данном случае конструктор просто вызывает метод InitializeComponent(). Это в действительности связано с Visual Studio.NET. Visual Studio NET имеет все те же свойства, что и IDE VB6 для графических манипуляций элементами управления — щелчок мышью для размещения элементов управления на форме и т.д. Однако, так как теперь в C# определения всех элементов управления задаются в исходном коде, Visual Studio.NET должна иметь возможность прочитать исходный код, чтобы определить, какие элементы управления находятся на форме. Она делает это, разыскивая метод InitializeComponent() и определяя, экземпляры каких элементов управления там создаются.

InitializeComponent() является большим методом, поэтому он будет показан здесь полностью. Начинается он следующим образом:

private void InitializeComponent() {

this.txtNumber = new System.Windows.Forms.TextBox();

this.txtSign = new System.Windows.Forms.TextBox();

this.cmdShowResults = new System.Windows.Forms.Button();

this.label3 = new System.Windows.Forms.Label();

this.label4 = new System.Windows.Forms.Label();

this.label1 = new System.Windows.Forms.Label();

this.label2 = new System.Windows.Forms.Label();

this.txtResult = new System.Windows.Forms.TextBox();

Показанный код является множеством вызовов для реального создания экземпляров всех элементов управления на форме. Этот фрагмент кода не содержит на самом деле ни одного нового элемента синтаксиса C#, который бы до сих пор не встречался. Следующая часть кода начинает задавать свойства элементов управления:

//

// txtNumber

//

this.txtNumber.Location = new System.Drawing.Point(160, 24);

this.txtNumber.Name = "txtNumber";

this.txtNumber.TabIndex = 0; this.txtNumber.Text = "";

//

// txtSign

//

this.txtSign.Enabled = false;

this.txtSign.Location = new System.Drawing.Point(160, 136);

this.txtSign.Name = "txtSign";

this.txtSign.TabIndex = 1;

this.txtSign.Text = "";

Этот код задаёт начальные позиции и начальный текст двух элементов управления, текстового поля ввода и текстового поля, которое выводит знак заданного числа. Новый элемент кода состоит в том что положение относительно верхнего левого угла экрана задается с помощью Point. Point является базовым классом .NET (строго говоря, структурой), который содержит x- и y-координаты. Синтаксис двух строк, задающих Location, является инструктивным. Свойство TextBox.Location является просто ссылкой на Point, поэтому, чтобы задать ему значение, необходимо создать и инициализировать объект Point, содержащий правильные координаты. Это первое использование нами конструктора с параметрами — в данном случае горизонтальной и вертикальной координат Point и, следовательно, элемента управления. Если было бы желательно транслировать одну из этих строк в VB, предполагая, что был определён некоторый модуль класса VB с именем Point, и мы имели бы класс, который имеет такое свойство, то лучшее, что можно было бы сделать, выглядело бы примерно следующим образом:

Dim Location As Point

Set Location = New Point

Location.X = 160

Location.Y = 24

SomeObject.Location = Location

Это сравнимо со следующим кодом на C#:

someObject.Location = new System.Drawing.Point(160, 24);

Относительная компактность эквивалентной инструкции C# должна быть очевидна.

Теперь рассмотрим те же команды для кнопки. В этом случае можно наблюдать задание свойств такого же вида, но здесь имеется другая вещь, которую необходимо выполнить: приказать Windows вызвать наш обработчик событий, когда нажимается кнопка. Это делает последняя строка из нижеследующих.

this.cmdShowResults.Name = "cmdShowResults";

this.cmdShowResults.Size = new System.Drawing.Size(88, 23);

this.cmdShowResults.TabIndex = 3;

this.cmdShowResults.Text = "Show Results";

this.cmdShowResults.Click +=

new System.EventHandler(this.OnClickShowResults);

Здесь происходит следующее. Кнопка, обозначенная как объект кнопки cmdShowResults, содержит событие Click, которое будет инициироваться, когда пользователь на нее нажмет. Надо добавить для этого события собственный обработчик событий. Сейчас C# не разрешает передавать имена методов непосредственно, вместо этого они должны помещаться в так называемый объект делегат. Детали этого здесь не рассматриваются, они приведены в главе 6 данной книги, но это делается для обеспечения безопасности типов. Вследствие такого действия появляется текст new System.EventHandler() в этом коде. Когда имя обработчика событий будет спрятано, мы добавим его к событию, используя оператор +=, который будет рассмотрен ниже.

Арифметические операторы присваивания

Символ += представляет в C# так называемый оператор сложения-присваивания. Он дает удобное сокращение для случаев, когда необходимо добавить некоторую величину к другой величине. Это работает следующим образом. Пусть в VB объявлены два целых числа А и В и необходимо записать следующее выражение:

В = В + А

В C# можно записать похожим образом:

В = В + А;

Однако в C# для этого существует альтернативная сокращенная запись:

B += А;

+= в действительности означает "сложить значение выражения справа с переменной слева" и это работает для всех числовых типов данных, а не только для целых. Существуют также другие аналогичные операторы *=, /= и -=, которые соответственно умножают, делят и вычитают величину слева из переменной справа. Поэтому, например, чтобы разделить число на 2 и присвоить результат снова В, можно написать:

B /= 2;

Хотя в этом приложении не рассматриваются подробности, но C# имеет другие операторы, представляющие побитовые операции, а также дающие остаток при делении, и почти все они имеют соответствующие операторы операция-присваивание (см. главу 3).

В примере SquareRootForm оператор сложения-присваивания применен к событию, строка:

this.cmdShowResults.Click +=

new SyBtem.EventHandler(this.OnClickShowResults)

означает: "добавить этот обработчик к событию". Может быть немного удивительным увидеть оператор, подобный +=, который применяется к чему-то, что не является таким простым числовым типом данных, как int или float, но это на самом деле иллюстрирует важный момент в отношении операторов в C# по сравнению с операторами в VB:

Операторы, подобные +, * и т.д. в VB действительно имеют значение, только когда применяются к числовым данным. Но в C# они могут применяться к объектам любого типа.

Приведенное выше утверждение необходимо немного уточнить. Чтобы применять эти операторы к другим типам объектов, необходимо сначала сообщить компилятору, что эти операторы означают для других типов объектов — процесс, называемый перезагрузкой операторов. Он работает примерно следующим образом. Предположим, что необходимо написать класс, который представляет, скажем, математический вектор. В VB это можно закодировать как модуль класса, который позволит написать:

Dim V1 As Vector

Set V1 = New Vector

В математике векторы можно складывать, что будет обеспечено с помощью перезагрузки операторов. Но VB6 не поддерживает перезагрузку, поэтому вместо этого в VB6 вероятно придется определить метод Add для Vector, и, таким образом, сделать следующее:

' V1, V2 и V3 являются векторами

Set V3 = V1. Add(V2)

В VB это лучшее, что можно придумать. Однако в C#, если определить класс Vector, можно добавить в него перезагруженный оператор для +, который является по сути методом, имеющим имя operator+, и который компилятор будет вызывать, если увидит +, примененный к Vector. Это означает, что в C# можно будет написать:

// V1, V2 и V3 являются векторами

V3 = V1 + V2;

Очевидно, что перезагруженные операторы не будут определяться для всех классов. Для большинства классов не будут иметь смысла действия типа сложения или умножения объектов. Однако для классов, для которых это имеет смысл, перезагрузка операторов может сделать код значительно проще для восприятия. Именно это и происходит с событиями. Поскольку имеет смысл говорить о добавлении обработчика к событию, был предоставлен перезагруженный оператор, чтобы позволить делать это, используя интуитивно понятный синтаксис с помощью операторов + (и +=). Можно также использовать - или -= для удаления обработчика из события.

Подводя итог

Мы получили максимум возможного из рассмотрения примеров SquareRootForm. Существует значительный объем кода C#. который не был рассмотрен в версии C# этого приложения, но этот дополнительный код связан в основном с заданием различных других элементов управления на форме, и не вводи никаких новых принципов, поэтому мы не заострили на нем внимание.

К этому моменту мы получили представление о синтаксисе C#. Мы видели, что он позволяет писать инструкции, которые значительно короче, чем соответствующий код VB. Мы также заметили, что C# помещает весь код в исходный файл в отличие от VB, где большая часть базового кода скрыта от программиста, что делает код проще за счет уменьшения гибкости при создании приложений. Мы также познакомились с концепцией наследования.

Однако мы пока еще не видели реального примера некоторого кода, который можно написать на C#, но крайне трудно создать код, делающий то же самое на VB. Мы собираемся рассмотреть такой пример в следующем разделе, где будут с помощью некоторых классов проиллюстрированы возможности наследования.

Пример: Employees и Managers

Для этого примера предположим, что пишется приложение, делающее некоторую обработку данных, имеющих отношение к сотрудникам компании. Для нас неважно, какую обработку оно включает, больший интерес представляет факт, что для этого достаточно полезно будет написать класс C# (или модуль класса VB), представляющий сотрудников. Мы предполагаем, что это будет формировать часть программного пакета, который можно продавать компаниям, чтобы помочь им при выплате зарплаты и т.д.

Модуль класса Employee в VB

Следующий код представляет попытку закодировать модуль класса Employee на VB. Модуль класса предоставляет два открытых свойства: EmployeeName и Salary, а также открытый метод GetMonthlyPayment(), возвращающий сумму, которую компания должна платить сотруднику каждый месяц. Это не совпадает с зарплатой частично потому, что зарплата предполагается выплачиваемой за год, и частично потому, что позже будет представлена возможность прибавления других выплат компании сотруднику (таких, как бонусы за производительность):

' локальные переменные для хранения значений свойств

Private mStrEmployeeName As String ' локальная копия

Private mCurSalary As Currency ' локальная копия

Public Property Let Salary(ByVal curData As Currency)

mCurSalary = curData

End Property

Public Property Get Salary() As Currency

Salary = mCurSalary

End Property

Public Property Get EmployeeName() As String

EmployeeName = mStrEmployeeName

End Property

Public Sub Create(sEmployeeName As String, curSalary As Currency)

mStrEmployeeName = sEmployeeName

mCurSalary = curSalary

End Sub

Public Function GetMonthlyPayment() As Currency

GetMonthlyPayment = mCurSalary/12

End Function

В реальной жизни будет написано, по-видимому, что-то более сложное, но и этого класса будет достаточно для иллюстрации рассматриваемых нами концепций. Фактически мы уже имеем проблему с этим модулем класса VB — имена большинства людей меняются не очень часто, вот почему свойство EmployeeName предназначено только для чтения. Это по-прежнему оставляет необходимость задавать имя в первый раз. Для этого добавлен метод Create, который определяет имя и зарплату. Таким образом, процесс создания объекта сотрудника будет выглядеть так:

Dim Britney As Employee

Set Britney = New Employee

Britney.Create "Britney Spears", 20000

Эта схема работает, но она не очень удобна. Проблема с инициализацией объекта Employee состоит в том, что хотя VB предоставляет для этой цели методы Class_Load и Class_Initialize, метод Class_Load не может получать никаких параметров. Это означает, что нельзя выполнить никакой инициализации, которая является специфической для данного экземпляра Employee, поэтому необходимо просто написать отдельный метод инициализации Create и надеяться, что все, кто пишет клиентский код, никогда не будут забывать его вызывать. Такое решение неудобно, так как нет никакого смысла иметь объект Employee, у которого не заданы имя и зарплата, но именно это присутствует в приведенном выше коде в течение короткого периода между созданием экземпляра Britney и инициализацией объекта. Пока будут помнить о вызове метода Create, все будет нормально, но здесь имеется потенциальный источник ошибок.

В C# ситуация совершенно другая. Здесь в конструкторы можно подставлять параметры (эквивалент в C# для метода Class_Load). Необходимо только убедиться, что при определении класса Employee в C# конструктор получает Name и Salary в качестве параметров. В C# можно будет написать:

Employee Britney = new Employee("Britney Spears", 20000.00M);

что значительно изящнее и менее подвержено ошибкам. Отметим кстати символ "М", добавленный к зарплате. Это связано с тем, что эквивалент C# для типа Currency из VB называется десятичным значением и 'M', добавленный к числу в C#, подчеркивает, что число надо интерпретировать как decimal. Его указывать не обязательно, но это полезно для дополнительной проверки во время компиляции.

Класс Employee в C#

Помня о приведенных выше замечаниях можно теперь представить первое определение версии C# класса Employee (отметим, что здесь показано определение класса, а не определение содержащего его пространства имен):

class Employee {

private readonly string name;

private decimal salary;

public Employee(string name, decimal salary) {

this.name = name;

this.salary = salary;

}

public string Name {

get {

return name;

}

}

public virtual decimal Salary {

get {

return salary;

}

set {

salary = value;

}

}

public decimal GetMonthlyPayment() {

return salary/12;

}

public override string ToString() {

return "Name: " + name + ", Salary: $" + salary.ToString();

}

}

Просматривая этот код, мы видим сначала пару закрытых переменных — так называемых полей-членов, соответствующих переменным-членам в модуле класса VB. Поле name помечено как readonly. Мы скоро узнаем его точное значение. Грубо говоря, это гарантирует, что данное поле задано, когда создавался объект Employee, и не может впоследствии изменяться. В C# обычно не используют "венгерский" стиль именования объектов для имен переменных, поэтому они просто называются name и salary, а не mStrEmployeeName и mCurSalary. "Венгерский" стиль именования объектов означает, что имена переменных имеют префикс из букв, который указывает их тип (mStr, mCur и т.д.). Это на сегодня неважно, так как редакторы являются более развитыми и могут автоматически предоставить информацию о типах данных. Поэтому рекомендуется не использовать "венгерский" стиль именования объектов в программах C#.

В классе Employee существует также конструктор, пара свойств — Name и Salary, а также два метода — GetMonthlyPayment() и ToString(). Все это будет рассмотрено далее.

Отметим кстати, что имена свойств Name и Salary отличаются только регистром символов от имен своих соответствующих полей. Это не является проблемой, так как C# различает регистр символов. Способ, которым здесь именованы свойства и поля, соответствует обычному соглашению в C# и показывает, как можно на самом деле воспользоваться различием регистра символов.

Конструктор Employee

После объявления полей в приведенном выше коде располагается "метод", имя которого — Employee, совпадает с именем класса, то есть перед нами находится конструктор. Однако этот конструктор получает параметры и делает то же самое, что и метод Create в версии VB — он использует параметры для инициализации полей-членов:

public Employee(string name, decimal salary) {

this.name = name;

this.salary = salary;

}

Существует потенциальная синтаксическая проблема, так как явные имена параметров совпадают с именами полей — name и salary. Но она разрешается с помощью использования ссылки this, помечающей поля. Можно было бы вместо этого дать параметрам другие имена, но способ, которым это было сделано, является достаточно ясным и означает, что параметры сохраняют очевидные простые имена, которые соответствуют их значениям. Это обычный способ действий для C# в таких ситуациях.

Теперь можно объяснить точное значение квалификатора readonly перед именем поля:

private readonly string name;

Если поле помечено как readonly, то единственным местом, где ему может быть присвоено значение, является конструктор класса. Компилятор будет инициировать ошибку, если встретит код, который попытается изменить значение переменной readonly, в любом месте, кроме конструктора. Это предоставляет надежную гарантию, что переменная не будет изменена, если она была задана. Невозможно сделать что-либо подобное в VB, так как VB не имеет конструкторов, которые получают параметры, поэтому переменные уровня класса в VB должны быть инициализированы с помощью методов или свойств, вызываемых после создания экземпляра объекта.

Между прочим этот конструктор не просто позволяет задать параметры для инициализации объекта Employee — он заставляет это сделать. Если написать код следующего вида:

Employee Britney = new Employee; // неправильно

то он на самом деле не откомпилируется. Компилятор будет инициировать ошибку, так как в C# должен всегда вызываться конструктор, когда создается новый объект. Но никаких параметров задано не было, а единственный доступный конструктор требует двух параметров. Поэтому просто невозможно создать объект Employee без каких-либо параметров. Это страхует от ошибок, вызываемых неинициализированными объектами Employee.

Можно задать в классе более одного конструктора, чтобы выбрать, какое множество желательно использовать при создании нового объекта этого класса. Мы увидим, как это делается позже в данном приложении. Однако для этого конкретного класса единственного конструктора вполне достаточно.

Свойства класса Employee

Теперь мы переходим к свойствам Name и Salary. Синтаксис C# для объявления свойства существенно отличается от соответствующего синтаксиса VB, но базовые принципы одинаковы. Необходимо определить два метода доступа (accessors) соответственно для "получения" и "задания" значений свойства. В VB они синтаксически интерпретируются как методы, но в C# свойство объявляется в целом, а затем определяются методы доступа внутри определения свойства.

public decimal Salary {

get {

return salary;

}

set {

salary = value;

}

}

В VB компилятор знает, что определяется свойство, так как используется ключевое слово Property. В C# эта информация передается тем, что за именем свойства немедленно следует открывающая фигурная скобка. Если определяется метод, то это будет открывающая скобка, указывающая начало списка параметров, в то время как для поля это будет точка с запятой, отмечающая конец определения.

Еще один момент, на который необходимо обратить внимание, состоит в том, что определения методов доступа get и set не содержат никаких списков параметров, это не важно. Мы знаем, что Salary является десятичным значением, и метод доступа get вернет десятичное значение, не используя параметры, в то время как метод доступа set будет получать один десятичный параметр и возвращать void. Для процедуры доступа set этот параметр не объявляется явно, но компилятор всегда интерпретирует слово value как ссылающееся на него.

Здесь снова синтаксис определения свойств показывает, что в случае C# он является более компактным и может облегчить ввод кода.

Так же как в VB, если необходимо сделать свойство предназначенным только для чтения, то просто опускается метод доступа set, как было сделано для свойства Name:

public string Name {

get {

return name;

}

}

Методы класса Employee

В классе Employee существуют два метода — GetMonthlySalary() и ToString().

GetMonthlySalary() не требует комментариев, так как большая часть соответствующего синтаксиса C# уже была рассмотрена. Берется зарплата, делится на 12 для преобразования из годовой в месячную зарплату, и возвращается результат:

public decimal GetMonthlyPayment() {

return salary/12;

}

Единственным новым элементом синтаксиса здесь является инструкция return. В VB возвращаемое из метода значение определяют, задавая требуемое значение фиктивной переменной, которая имеет такое же имя, как и функция

GetMonthlyPayment = mCurSalary/12

В C# тот же самый результат получают, добавляя параметр в инструкцию return (без скобок). Также return в C# определяет, что происходит выход их функции, поэтому инструкция C#:

return salary/12;

эквивалентна в действительности следующему коду VB:

GetMonthlyPayment = mCurSalary/12

Exit Function

Метод ToString() более интересен. В большинстве случаев при написании класса C# будет полезным создание метода ToString(), который может использоваться для получения быстрого просмотра содержимого объекта. Как упоминалось ранее, метод ToString() уже доступен, так как все классы наследуют его от System.Object. Однако версия в System.Object выводит только имя класса и никаких данных из экземпляра класса. Компания Microsoft уже переопределила этот метод для всех числовых типов данных (int, float и т.д.), чтобы выводить реальные значения переменных, и нелишне будет сделать то же самое для собственных классов программиста. В любом случае это может быть полезно для просмотра содержимого объекта во время отладки:

publiс override string ToString() {

return Name: " + name + ", Salary: $" + salary.ToString();

}

Эта переопределенная версия выводит имя и зарплату сотрудника. Новым элементом синтаксиса является то, что метод специально объявлен как override. C# требует, чтобы переопределяемые версии методов помечались явно, и будет инициировать ошибку компиляции, если этого не сделать. Это исключает риск любых потенциальных ошибок, когда, например, мы можем случайно переопределить метод без его реализации, возможно не зная, что метод с таким именем уже присутствует в базовом классе.

Мы завершили пример класса Employee как в VB, так и в C#, и до сих пор, хотя имеются некоторые неровности в создании и инициализации экземпляра Employee в версии VB, оба языка справились достаточно хорошо с требованиями. Однако одна из целей этого приложения состоит в том, чтобы показать, почему C# может быть в отдельных ситуациях значительно более мощным, чем VB6. Мы будем добавлять некоторые свойства в версию C# нашего примера, которые оставят VB далеко позади. Начнем со статических полей и свойств.

Статические члены

Мы упоминали несколько раз, что в C# классы имеют специальные методы, называемые статическими, которые можно вызвать, не создавая экземпляр объекта. Эти методы не имеют никакой аналогии в VB. Фактически, статическими могут быть не только методы, но и поля, свойства или любые другие члены класса.

Термин статический имеет совершенно другое значение в C#, чем в VB.

Чтобы проиллюстрировать, как работают статические члены и почему их необходимо использовать, давайте представим себе, что мы хотели бы, чтобы класс Employee поддерживал извлечение названия (имени) компании, в которой работает каждый сотрудник. Здесь имеется существенное различие между названием компании и именем сотрудника, так как каждый объект сотрудника представляет обособленную единицу, и поэтому необходимо хранить различные имена сотрудников. Это обычное поведение переменных модулей классов в VB и поведение по умолчанию полей в C#. Но если организация купила программное обеспечение, которое содержит класс Employee, то очевидно, что на всех сотрудников приходится одно и то же название компании. Это означает, что было бы избыточно хранить имя компании отдельно для каждого сотрудника. Будет просто ненужное дублирование строки. Вместо этого мы хотим сохранить имя компании только один раз и затем предоставить доступ к этим данным каждому объекту сотрудника. Именно так работает статическое поле. Объявим такое поле как companyName:

class Employee {

private string name;

private decimal salary;

private static readonly string companyName;

В этом коде объявлено еще одно поле, но, помечая его как static, мы инструктируем компилятор, что эту переменную нужно сохранить только один раз, независимо от того, сколько создано объектов Employee. В реальном смысле это статическое поле ассоциируется с классом как целым, а не с каким-то одним объектом.

Мы также объявили это поле используемым только для чтения. Такое указание имеет смысл, потому что название компании, так же как имя сотрудника, не должно меняться после запуска программы.

Конечно, одного объявления этого поля не достаточно. Необходимо также убедиться, что оно инициализируется правильными данными. Где это нужно сделать? Ясно, что не в конструкторе — конструктор вызывается всякий раз при создании объекта Employee, в то время как companyName нужно инициализировать только однажды. C# предоставляет для этой цели так называемый статический конструктор, который действует как любой другой конструктор, но работает для класса в целом, а не для определенного объекта. Если для класса определить статический конструктор, то он будет выполняться только один раз. Не гарантируется точно, когда он сработает, но это произойдет до того, как любой клиентский код попытается получить доступ к классу. Это обычно происходит при первом запуске программы. Добавим статический конструктор для класса Employee:

static Employee {

companyName = "Wrox Press Pop Stars";

}

Как обычно, конструктор идентифицируется по имени, которое совпадает с именем класса. Этот конструктор обозначается также как static, следовательно, он является статическим конструктором. Он не помечен ни как public, ни как private, так как он не будет вызываться никаким кодом C#, а только средой выполнения .NET. Поэтому для статического конструктора не требуется модификатор доступа.

В нашем примере статический конструктор был реализован с жестко закодированным названием компании. Еще реальнее было бы прочитать запись в реестре или файл или соединиться с базой данных, чтобы найти название компании. Между прочим, поскольку поле companyName объявлено как статическое и только для чтения, то статический конструктор является единственным местом, где полю можно законно присвоить значение. Осталось сделать одну последнюю вещь — определить открытое свойство, которое позволяет получить доступ к названию компании.

public static string сompanyName {

get {

return companyName;

}

}

Свойство companyName также было объявлено как статическое, и теперь можно видеть реальное значение статическою метода или свойства метод или свойство может быть объявлен как статический, если он обращается только к статическим полям, и не обращается ни к каким данным, которые ассоциируются с определенным объектом.

Как мы уже видели, синтаксис вызова статических членов класса извне класса слегка отличается от используемого для других членов, так как статический член ассоциирован с классом, а не с каким-то объектом, то для его вызова используется имя класса, а не имя переменной:

string Company = Employee.CompanyName;

Концепция статических членов является очень мощной и предоставляет полезные средства классу для реализации любой функциональности, которая является одинаковой для каждого объекта этого класса. Единственным способом, которым можно добиться чего-то подобного в VB, является определение глобальных переменных. Однако, если сделать это, то глобальные переменные имеют недостаток, заключающийся в том, что они не связаны с каким-то классом, что ведет также к вопросам конфликта имен.

Другими ситуациями, где используются статические члены класса, являются:

□ Возможная реализация свойства MaximumLength для нашего класса Employee или любого другого класса, содержащего имя, если необходимо определить максимальную длину имени.

□ В C# большинство цифровых типов данных имеют статические свойства, которые указывают их максимальные значения. Можно, например, определить наибольшие значения, которые хранятся в int и float:

int MaxIntValue = int.MaxValue;

float MaxFloatValue = float.MaxValue;

Наследование

Теперь мы собираемся с помощью примера рассмотреть, как работает реализация наследования. Давайте предположим, что около года назад был поставлен пакет программного обеспечения и пришло время для следующей версии. Заказчики сделали замечание, что некоторые из их сотрудников являются на самом деле менеджерами, а менеджеры обычно получают бонусы в зависимости от прибыли, а также обычною зарплату. Это означает что метод GetMonthlyPayment() не дает полной информации для менеджеров. Вывод: необходимо добавить некоторый способ учета менеджеров.

Для целей нашего примера будем предполагать, что бонус является некоторой постоянной величиной, которую можно определить при создании менеджера. Мы откажемся в данном случае выполнять какие-либо специальные вычисления, связанные с прибылью.

Как можно при создании кода в VB обновить наше приложение? Существуют два возможных подхода, но оба они имеют серьезные недостатки. Можно:

□ Написать новый класс Manager

□ Изменить класс Employee

Создание нового класса является, вероятно, тем подходом, который потребует меньше всего работы так как можно начать с простого копирования кода модуля класса Employee и затем изменить эту копию кода. Проблема состоит в том, что Employee и Manager имеют такой большой объем общего кода, как весь код, связанный со свойствами Name, CompanyName и Salary. Дублирование одного и того же кода является опасным. Что произойдет, если в некоторый момент какая-то причина заставит изменить код? Плохой разработчик надеется не забыть внести одинаковые изменения в оба класса. Это самый простой способ создания ошибок. Другая проблема состоит в том, что теперь существуют два несвязанных класса Employee и Manager, с которыми должен иметь дело код клиента, что скорее всего сделает его написание затруднительным. (Хотя можно обойти эту проблему, помещая общие свойства в интерфейс и реализуя этот интерфейс в обоих классах Employee и Manager.)

Альтернативным способом является написание класса Manager и размещение объекта Employee внутри него как переменной с областью действия класса. Это решает проблему дублирования кода, но по-прежнему оставляет нас с двумя различными объектами, а также с неудобным, непрямым синтаксисом для вызова методов и свойств сотрудника (objManager.objEmployее.Name и т.д.).

Если выбрать модификацию модуля класса сотрудника, то по-видимому надо добавить дополнительное поле типа Boolean, которое указывает, является ли Employee менеджером или нет. Затем в соответствующих частях кода это Boolean будет проверяться в инструкции if, чтобы знать, что делать. Это решает проблему двух несвязанных классов — снова имеется только один класс. Однако это вносит новую трудность: как было специально сказано ранее, поддержку для менеджеров решено было добавить примерно год спустя. Это означает, что модуль класса Employee был по-видимому поставлен, протестирован, полностью отлажен и известно, что он работал правильно. В этой ситуации вряд ли возникнет желание обращаться к работающему коду, чтобы изменить его, учитывая связанный с этим риск внесения новых ошибок.

Другими словами, мы достигли точки, где VB не может предложить никакого удовлетворительного решения. Из заголовка этого раздела нетрудно сделать заключение, что C# предлагает способ решения этой проблемы с помощью использования наследования.

Мы уже видели, что наследование включает добавление или замену свойств классов. В предыдущем примере класс SquareRootForm добавил код к классу .NET System.Windows.Forms.Form. Он определил элементы управления для размещения в SquareRootForm как поля-члены, а также добавил обработчик событий. В примере Employee будут продемонстрированы как добавление, так и замена свойств базового класса, а также определен класс Manager, который является производным из класса Employee. Мы добавим поле и свойство, представляющие бонус, и заменен метод GetMonthlyPayment() (для полноты также будет заменен метод ToString(), чтобы он выводил бонус вместе с именем и зарплатой). Все это означает, что будет получен отдельный класс. Но при этом не потребуется дублировать никакой код и вносить большие изменения в класс Employee. Может показаться, что по-прежнему существует проблема двух различных классов, что делает более трудным написание клиентского кода, но, как будет продемонстрировано позже, C# имеет ответ и на это.

Наследование от класса Employee

Прежде чем определить класс Manager(), необходимо внести одно маленькое изменение в классе Employee:

public virtual decimal GetMonthlyPayment() {

return salary/12;

}

что сделает метод GetMonthlyPayment() виртуальным (virtual). Это способ, которым C# сообщает, что данный метод в принципе может быть переопределен.

Можно подумать, что это означает изменение базового класса, что противоречит тезису о ненужности изменения базового класса. Однако добавление ключевого слова virtual на самом деле не является изменением, которое влечет за собой риск новых ошибок — при подходе VB необходимо было действительно переписать реализации нескольких методов. Кроме того, обычно при создании классов в C# заранее планируется, какие методы являются подходящими для переопределения. Если бы это был пример из реальной жизни, то метод GetMonthlyPayment() почти наверняка объявлялся бы виртуальным, поэтому на самом деле можно добавить класс Manager, не делая никаких изменений в классе Employee.

Класс Manager

Теперь можно определить класс Manager():

class Manager : Employee {

private decimal bonus;

public Manager(string name, decimal salary, decimal bonus) : base(name, salary) {

this.bonus = bonus;

}

public Manager(string name, decimal salary) : this(name, salary, 100000M) {

}

public decimal Bonus {

get {

return bonus;

}

}

public override string ToString() {

return base.ToStrint() + ", bonus: " + bonus;

}

public override decimal GetMonthlyPayment() {

return base.GetMonthlyPayment() + bonus/12;

}

}

Помимо почти завершенной реализации класса Employee, который был унаследован, Manager содержит следующие члены:

□ Поле bonus, которое будет использоваться для хранения бонуса менеджера, и соответствующее свойство.

□ Перезагруженный метод GetMonthlyPayment(), а также новую перегруженную версию метода ToString().

□ Два конструктора.

Поле bonus и соответствующее свойство Bonus не требуют дальнейших обсуждений. Однако мы внимательно рассмотрим переопределенные методы и новые конструкторы, так как они будут иллюстрировать важные свойства языка C#.

Переопределение метода

Переопределенная версия метода GetMonthlyPayment() является достаточно простой. Отметим, что она помечена ключевым словом override для сообщения компилятору, что мы переопределяем метод базового класса, как это делалось с методом Employee.ToString():

public override decimal GetMonthlyPayment() {

return base.GetMonthlyPayment() + bonus/12;

}

Переопределенная версия содержит также вызов версии этого метода из базового класса. При этом используется новое ключевое слово base, base действует таким же образом, как и this, за исключением того, что оно специально указывает, что надо использовать метод или свойство и т.д. из определения базового класса. При желании можно альтернативно реализовать переопределенную версию метода GetMonthlyPayment() следующим образом:

public override decimal GetMonthlyPayment() {

return (Salary + bonus)/12;

}

но, чтобы показать использование ключевого слова base, был выбран другой вариант. В связи с этим есть одно действие, которое мы не смогли бы сделать:

public override decimal GetMonthlyPayment() {

return (salary + bonus)/12; // неправильно

}

Код выглядит почти так же, как предыдущая версия, кроме того, что поле salary используется непосредственно, а не через свойство Salary. Можно предположить, что это более эффективное решение, поскольку фактически убирается вызов метода. Но компилятор будет инициировать ошибку, так как поле salary объявлялось как private (закрытое). Этот означает, что ничему вне класса Employee не разрешается видеть это поле. Даже производные классы не знают о закрытых полях базового класса.

Если необходимо, чтобы производные, но не связанные классы могли видеть поле, C# предоставит альтернативный уровень защиты protected (защищенный):

protected decimal salary; // можно сделать так

Если член класса объявлен как защищенный, то он виден только в этом классе и в производных классах. Однако обычно строго рекомендуется сохранять все поля закрытыми (private) по той же причине, по которой требуется сохранять переменные закрытыми в модулях классов VB. Дело в том, что при сокрытии реализации класса (или модуля класса) облегчается выполнение будущего обслуживания этого класса. Обычно модификатор protected используется для свойств и методов, которые предназначены только для того, чтобы разрешать производным классам получать доступ к свойствам определения базового класса.

Конструкторы класса Manager

Давайте добавим по крайней мере один конструктор для класса Manager в связи с тем, что:

□ Существует дополнительный элемент информации — бонус менеджера, который необходимо определить, когда создается экземпляр объекта Manager.

□ В отличие от методов, свойств и полей, конструкторы не наследуются производными классами.

Фактически было добавлено два конструктора. Это связано с решением, что бонус менеджера обычно по умолчанию равен $100000, если он не определен явно. В VB можно определить в методах параметры по умолчанию, но C# не разрешает делать это напрямую. Вместо этого C# предлагает более мощную технику — перезагрузку методов, которая дает тот же результат. Определение в данном случае двух конструкторов позволяет проиллюстрировать эту технику.

Первый конструктор Manager имеет три параметра:

public Manager(string name, decimal salary, decimal bonus) : base(name, salary) {

this.bonus = bonus;

}

Прежде всего отметим вызов конструктора базового класса с помощью немного странного синтаксиса. Этот синтаксис называют инициализатором конструктора (constructor initializer.) При этом любому конструктору разрешается вызвать один другой конструктор перед своим выполнением. Этот вызов делается в инициализаторе конструктора с помощью показанного выше синтаксиса. Конструктор может вызвать либо другой конструктор того же класса, либо конструктор базового класса, что сделано с целью обеспечения хорошо спроектированной архитектуры конструкторов. Связанные с этим вопросы обсуждаются в главе 5. Синтаксис инициализатора конструктора требует двоеточия, за которым следует одно из ключевых слов base или this для определения, из какого класса вызывается второй конструктор, за которым следуют параметры, передаваемые этому второму конструктору.

Показанный выше конструктор получает три параметра. Однако два из них — name и salary, присутствуют там только для того, чтобы инициализировать поля базового класса в Employee. Эти параметры относятся на самом деле к классу Employee, а не Manager, поэтому они просто передаются конструктору Employee, с помощью чего делается вызов base(name, salary). Как мы видели раньше, конструктор Employee будет просто использовать эти параметры для инициализации полей name и salary. Наконец, мы передаем параметр bonus, имеющий отношение к классу Manager, и используем для инициализации поля bonus. Второй предоставленный конструктор Manager также применяет список инициализации конструктора:

public Manager(string name, decimal salary) : this(name salary, 100000M) {

}

В данном случае задается значение для параметра по умолчанию и затем все передается в конструктор с тремя параметрами. Конечно, в свою очередь, конструктор с тремя параметрами будет вызывать конструктор базового класса для работы с параметрами name и salary. Можно захотеть узнать, почему не использовался следующий альтернативный способ реализации конструктора с двумя параметрами:

public Manager(string name, decimal salary) : base(name, salary) // не очень хорошо

{

this.bonus = 100000M;

}

Причина в том, что это приводит к некоторому потенциальному дублированию кода: два конструктора каждый по отдельности инициализируют поле bonus, что может вызывать проблемы в будущем, если понадобится изменить оба конструктора, если в будущей версии Manager изменится способ хранения bonus. Обычно в C#, так же как и VB, стараются по возможности избегать дублирования кода. Таким образом, предыдущая реализация двухпараметрического конструктора считается более предпочтительной.

Перезагрузка методов

Тот факт, что для класса Manager было предоставлено два конструктора, иллюстрирует принцип перезагрузки методов в C#, в соответствии с которым предполагается, что класс имеет более одного метода с одним именем, но эти методы имеют различное число параметров. Мы продемонстрировали перезагрузку конструкторов, но точно такие же принципы применимы ко всем методам.

Не путайте термины перезагрузка и переопределение методов. Это различные и никак не связанные концепции.

Когда компилятор встречает вызов перезагруженного метода, он проверяет передаваемые параметры, чтобы определить, какой метод необходимо вызвать. В случае создания объекта менеджера, так как один конструктор получает три параметра, а другой только два, то компилятор прежде всего проверит число параметров. Следовательно, если написать:

Manager SomeManager = new Manager("Name", 300000.00M);

компилятор будет использовать для создания экземпляра объекта  Manager конструктор с двумя параметрами, то есть bonus будет присвоено значение по умолчанию, равное 100000M. Если, с другой стороны, написать:

Manager SomeManager = new Manager("Name", 300000.00М, 50000.00М);

компилятор использует конструктор с тремя параметрами, поэтому bonus получит указанное значение 50000.00М. При наличии нескольких доступных перезагружаемых версий компилятор не сможет найти подходящую и проинициирует ошибку компиляции. Например, если написать.

Manager SomeManager = new Manager(100, 300000.00М, 50000.00М); // неправильно

то будет получена ошибка компиляции, так как оба доступных конструктора Manager требуют строку, а не числовой тип в качестве первого параметра. Компилятор C# может организовать некоторый тип преобразований между различными числовыми типами, которые будут выполняться автоматически, но он не будет автоматически преобразовывать из числового значения в строку.

Наконец, отметим, что C# не разрешает методам использовать параметры по умолчанию, как это делает VB. Однако легко получить тот же самый эффект с помощью перезагрузки методов, как это сделано в нашем примере. Обычный способ состоит просто в использовании перезагруженных версий, которые имеют меньше параметров, чтобы подставить значения по умолчанию для оставшихся параметров и затем вызывать другие перезагружаемые версии.

Использование классов Employee и Manager

Теперь, когда завершено определение классов Employee и Manager, напишем код, который их использует. Фактически, если загрузить исходный код этого проекта с web-сайта издательства Wrox press, то можно выяснить, что два эти класса определены как часть стандартного проекта форм Windows, достаточно похожего на пример SampleRoot. В данном случае, однако, основная форма имеет только один элемент управления — поле списка. Мы используем конструктор класса основной формы (класса с именем MainForm) для создания экземпляров объектов Employee и Manager, а затем выводим данные этих объектов в поле списка. Результат представлен ниже:

Код, используемый для создания этого вывода, выглядит следующим образом:

public MainForm() {

InitializeComponent();

Employee Britney = new Employee("Britney Spearse", 20000.00M);

Employee Elton = new Manager("Elton John", 50000.00M);

Manager Ginder = new Hanager("Geri Halliwell", 50000.00M, 20000.00M);

this.listBox1.Items.Add("Elton's name is $" + Elton.Name);

this.listBox1.Items.Add("Elton's salary is $" + Elton.Salary);

this.listBox1.Items.Add("Elton's bonus is " + ((Manager)Elton).Bonus);

this.listBox1.Items.Add("Elton's monthly payment is $" + Elton.GetMonthlyPayment());

this.listBox1.Items.Add("Elton's Company is " + Employee.CompanyName);

this.listBox1.Items.Add("Elton.ToString() : " + Elton.ToString());

this.listBox1.Items.Add("Britney.ToString(): " + Britney.ToString());

this.listBox1.Items.Add("Ginger.ToString(): " + Ginger.ToString());

}

Этот код должен быть вполне понятен, так как использует элементы C#, с которыми мы уже знакомы, за исключением одной небольшой странности — один из объектов Manager обозначен ссылкой Employee, а не ссылкой Manager. Мы объясним, как это работает, дальше.

Ссылки на производные классы

Подробнее рассмотрим класс Manager, на который ссылается переменная, объявленная как ссылка на Employee:

Employee Elton = new Manager("Elton John", 50000.00M);

Это на самом деле совершенно законный синтаксис C#. Правило вполне простое: если объявлена ссылка на некоторый тип данных В, то этой ссылке разрешается ссылаться на экземпляры В или экземпляры любого производного из В класса. Это работает, так как любой класс, производный из В, должен также реализовать все методы или свойства и т.д., которые реализует класс В. Поэтому в примере выше вызывается Elton.Name, Elton.Salary и Elton.GetMonthlyPayment(). Тот факт, что Employee реализует все эти члены, гарантирует, что любой класс, производный из Employee, также будет это делать. Поэтому не имеет значения, указывает ли ссылка на производный класс — мы по-прежнему сможем использовать эту ссылку для вызова любого члена класса, на который определена ссылка, и будем уверены, что этот метод существует в производном классе.

С другой стороны, отметим синтаксис, который использовался при вызове свойства Bonus на объекте Elton: ((Manager)Elton).Bonus. В этом случае необходимо явно преобразовать Elton в ссылку на Manager, так как Bonus не реализовано в Employee. Компилятор знает это и будет создавать ошибку компиляции, если попробовать вызвать Bonus через ссылку на Employee. Данная строка кода является на самом деле сокращением записи:

Manager ManagerElton = (Manager)Elton;

this.listBox1.Items.Add("Elton's bonus is " + ManagerElton.Bonus);

Как и в VB, преобразование между типами данных в C# называется преобразованием типов (casting). Можно заметить в приведенном выше коде, что синтаксис преобразования типов включает размещение имени типа данных в скобках перед именем переменной, преобразование которой собираются выполнить. Конечно, указанный объект должен содержать прежде всего правильный тип данных. Если в этом примере написать:

Manager ManagerBritney = (Manager)Britney;

то код будет компилироваться правильно, но при его работе будет получена ошибка, так как среда выполнения .NET определит, что Britney является только экземпляром Employee, а не Manager. Ссылкам разрешается ссылаться на экземпляры производных классов, но не на экземпляры базовых классов своего собственного типа. Не разрешается ссылке на Manager ссылаться на объект Employee. (Это недопустимо, так как подумайте, что произойдет, если попытаться вызвать свойство Bonus с помощью такой ссылки.)

Кстати, совершенно не рассматривались подробности возникновения ошибки во время выполнения. На самом деле C# имеет для такого случая очень развитый механизм, называемый исключениями, который кратко будет показан позже.

Так как VB не поддерживает наследование реализации, то не существует прямой параллели в VB для поддержки ссылок, указывающих на объекты производных классов, как в C#. Однако это напоминает VB — можно объявить ссылку на интерфейс, при этом не имеет значения, на какой тип объекта ссылается интерфейс, пока этот объект реализует интерфейс. Если бы классы Employee и Manager кодировались в VB, можно было вполне сделать так, определяя интерфейс IEmployee, который реализуют оба модуля классов, и затем обращаться к свойствам Employee через этот интерфейс.

Массивы объектов

Важным достоинством ссылок, способных указывать на экземпляры производных классов, является то, что можно формировать массивы объектных ссылок, где различные объекты массива имеют различные типы. Это аналогично ситуации в Visual Basic, где можно сформировать массив ссылок на интерфейсы и не беспокоиться о том факте, что эти интерфейсные ссылки реализуются совершенно различными классами объектов.

Мы не видели еще, как C# работает с массивами, поэтому перепишем код классов Employee и Manager, чтобы сформировать массив объектных ссылок. Этот пересмотренный код можно также загрузить с web-сайта издательства Wrox Press, как пример EmployeeMaragerWithArrays. Новый код выглядит следующим образом:

public MainForm() {

InitializeComponent();

Employee Britney = new Employee("Britney Spears", 20000.00M);

Employee Elton = new Manager("Elton John", 50000.00M);

Manager Ginger = new Manager("Geri Halliwell", 50000.00M, 20000.00M);

Employee[] Employees = new Employee[3];

Employees[0] = Britney;

Employees[1] = Elton;

Employees[2] = Ginger;

for (int I = 0; I < 3; I++) {

this.listBox1.Items.Add(Employees[I].Name);

this.listBox1.Items.Add(Employees[I].ToString());

this.listBox1.Items.Add("");

}

}

Мы вызываем свойство Name и метод ToString() каждого элемента массива. Выполнение кода создает следующий результат.

Приведенный код показывает, что C# при работе с массивами использует квадратные скобки. Это означает, что в отличие от VB, не существует опасности какой-либо путаницы между массивом и вызовом метода или функции. Синтаксис для объявления массива выглядит так:

Employee[] Employees = new Employee[3];

Мы видим, что массив переменных некоторого тип объявляют, помещая квадратные скобки после имени типа. Массив в C# всегда считается ссылочным объектом (даже если его элементы являются простыми типами, как int или double), поэтому на самом деле существует два этапа: объявление ссылки и создание экземпляра массива. Чтобы сделать это понятнее, разделим приведенную выше строку кода следующим образом:

Employee[] Employees;

Employees = new Employee[3];

He существует разницы между тем, что делается здесь и созданием экземпляров объектов, за исключением того, что используются квадратные скобки для указания, что это массив. Отметим также, что размер массива определяется, когда создается экземпляр объекта, сама ссылка не содержит данных о размере массива — только его размерность. Размерность определяется любым количеством запятых в объявлении массива, поэтому, например, если надо объявить двухмерный, 3×4 массив чисел типа double, можно написать:

double [,] DoubleArray = new double[3, 4];

Есть и другие несущественные различия в синтаксисе объявления массивов, но мы будем придерживаться приведенных здесь правил. Когда имеется массив, то значения его элементам присваиваются обычным образом. Отметим, однако, одно различие между C# и VB, состоящее в том, что C# всегда начинает с элемента с индексом 0. В VB имеется возможность изменить его на индекс 1, используя инструкцию Option Base. Также в VB можно определить любую нижнюю границу для любого конкретного массива. Но это свойство не добавляет на самом деле никаких преимуществ и может снизить производительность, так как это означает, что при любом доступе к элементу массива в VB, код должен выполнить дополнительную проверку, чтобы определить, какова нижняя граница массива. C# такое действие не поддерживает.

В приведенном выше коде после инициализации элементов массива мы перебираем их в цикле. Необычный синтаксис цикла for будет скоро рассмотрен.

Отметим, что поскольку массив был объявлен как массив объектов Employee, то можно получить доступ только к тем членам каждого объекта, которые определены для класса Employee. Если требуется доступ к свойству Bonus любого объекта массива, то необходимо сначала преобразовать соответствующую ссылку в ссылку Manager, что будет означать проверку того, что объект на самом деле является менеджером. Это не трудно сделать, но данный вопрос находится за пределами рассмотрения настоящего приложения.

С другой стороны, хотя используются ссылки Employee, мы всегда выбираем правильную версию метода ToString(). Если указанный объект является объектом Manager, то при вызове метода ToString() для этого объекта будет выполняться версия метода ToString(), определенная в классе Manager. В этом состоит достоинство перезагрузки методов в C#. Можно заменить некоторые методы в производном классе и знать, что независимо от того, какой ссылочный тип используется для доступа к этому объекту, для него всегда будет выполняться правильный метод.

Цикл for

Давайте теперь рассмотрим странный синтаксис цикла for. Этот цикл является эквивалентом C# для следующего кода VB:

Integer I

For I = 1 То 3

listBox1.Items.Add "Details of the Employee"

Next

Идея цикла For в VB состоит в том, что он начинается с инициализации некоторой переменной — управляющей переменной цикла, и каждый раз при проходе цикла что-то добавляется к управляющей переменной, пока она не превысит конечное значение. Это достаточно полезно, но не дает почти никакой гибкости в работе цикла. Хотя можно изменять значение приращения или даже сделать его отрицательным, используя возможности Step, цикл всегда работает с помощью вычислений, и проверка на выход из цикла всегда происходит по достижении переменной некоторого минимального или максимального значения.

В C# цикл for обобщает эту концепцию. Базовая идея цикла for в C# следующая. В начале цикла что-то делается, на каждом шаге цикла тоже что-то делается, чтобы перейти к следующей итерации, затем, чтобы определить, когда выходить из цикла, выполняется некоторая проверка. Сравнение версий Visual Basic и C# выглядит следующим образом:

VB

C#

В начале цикла

Инициализация управляющей переменной цикла

Выполнить что-то

Проверка на выход из цикла

Не превысила ли переменная цикла некоторого значения?

Проверка некоторого условия

В конце каждой итерации

Увеличить управляющую переменную цикла

Выполнить что-то

Это может выглядеть как-то неопределенно, но зато дает большую гибкость. Например, в C# вместо добавления некоторой величины к управляющей переменной цикла на каждой итерации можно добавлять какое-то число, которое считывается из файла и которое изменяется на каждой итерации. Проверка не должна быть проверкой значения управляющей переменной цикла, это может быть проверка, например, достижения конца файла. Это позволяет при подходящем выборе начального действия, проверки и действия в конце каждой итерации циклу for эффективно выполнять те же задачи, что и любому из циклов VB — For, Foreach, Do и While, или цикл может вообще работать неким экзотическим образом, для которого нет простого эквивалента в VB. Цикл for в C# действительно представляет полную свободу управления циклом в том виде, какой будет необходим для рассматриваемой задачи.

Надо отметить, что C# поддерживает также циклы foreach, do и while для соответствующих ситуаций.

Давайте вернемся к точному синтаксису. Вспомним, что версия C# для приведенного выше цикла for выглядит следующим образом:

for (int I = 0; I < 3; I++) {

this.listBox1.Items.Add(Employees[I].Name);

this.listBox1.Items.Add(Employees[I].ToString());

this.listBox1.Items.Add("");

}

Как можно видеть, инструкция for получает три различные элемента внутри скобок. Эти элементы разделяются точкой с запятой:

□ Первый элемент является действием, которое выполняется прямо в начале цикла, чтобы инициализировать цикл. В данном случае объявляется и инициализируется управляющая переменная цикла.

□ Следующим элементом является условие, определяющее завершение цикла. В данном случае условие состоит в том, что I должно быть меньше 3. Цикл продолжается, пока это условие будет true, и закончится, как только условие станет false. Условие оценивается в начале каждой итерации, чтобы, если оно окажется false в самом начале, инструкция внутри цикла вообще не выполнялась.

□ В третьем элементе находится инструкция, которая выполняется в конце каждой итерации цикла. Циклы Visual Basic всегда работают, увеличивая некоторое число, и это как раз то, что делается в данном случае.

Синтаксис выглядит незнакомым, но после привыкания к нему, можно использовать цикл for очень эффективно. Например, предположим, что необходимо вывести все целые степени 2, которые меньше 4000, в окне списка. Запишем следующий код:

for (int I = 2; I < 4000; I *= 2)

listBox1.Items.Add(I.ToString());

Такой результат можно получить и в VB, но немного труднее. Для этого конкретного цикла в VB лучше воспользоваться циклом while.

Другие свойства C#

Мы закончили рассмотрение примеров. Оставшаяся часть приложения кратко рассматривает несколько свойств C#, о которых необходимо знать при выполнении перехода от VB к C#, и которые еще не рассматривались, в частности, некоторые из концепций C#, связанные с типами данных и операторами.

Типы данных

Как указывалось ранее, доступные в C# типы данных отличаются от типов данных, доступных в Visual Basic, лишь деталями. Но не только это, все типы данных в C# имеют свойства, которые обычно связываются с объектами. Например, как мы видели, каждый тип данных, даже простые типы, такие как int и float, поддерживает вызов методов (кстати, это свойство не вызывает никакой потери производительности).

Хотя типы данных, доступные в C#, слегка отличаются от типов данных в VB, все же большинство типов данных, знакомые из VB, имеют непосредственные эквиваленты в C#. Например, вместо Double из VB, C# имеет double, вместо Date из VB , C# имеет базовый класс .NET DateTime, который реализует огромное число методов и свойств, позволяющих извлекать или задавать даты с помощью различных форматов.

Одним исключением является Variant, для которого нет прямого эквивалента в C#. Тип данных Variant в VB является очень общим типом данных, который в некоторой степени существует только, чтобы поддерживать языки сценариев, которые не знают никаких других типов данных. Философия C#, однако, состоит в том, что язык является строго типизированным. Главная идея: если в каждом месте программы явно указывать используемый тип данных, то будет исключен один из основных источников ошибок времени выполнения. В связи с этим тип данных Variant в действительности не соответствует C#. Но все равно существуют ситуации, в которых необходимо ссылаться на переменную, не указывая тип этой переменной, и для этих случаев C# имеет тип данных object. object в C# очень похож на Object в VB. Однако в VB Object ссылается конкретно на объект COM и поэтому может использоваться только для ссылок на объекты, которые в терминологии VB означают ссылочные типы данных. Нельзя, например, использовать объектную ссылку для ссылки на Integer или на Single. В C#, напротив, объекты могут использоваться для ссылки на любой тип данных .NET, и так как в C# все типы данных являются типами данных .NET, это означает, что вы в праве преобразовать что угодно в object, включая int, float и все предопределенные типы данных. В этом смысле object в C# выполняет роль, аналогичную Variant в VB.

Типы данных значений и ссылочные типы данных

В Visual Basic существует четкое различие между типами данных значений и ссылочными типами данных. Типы данных значений включают большинство предопределенных типов данных: Integer, Single, Double и даже Variant (хотя строго говоря Variant может содержать также ссылку). Ссылочными типами данных являются любые объекты, включая определяемые модули классов и объекты ActiveX. Как можно было заметить из примеров в этом приложении, C# также делает различие между типами данных значений и ссылочными типами данных. Однако C# допускает больше гибкости, позволяя при определении класса найти, что этот класс будет типом данных значений. Это делается с помощью объявления класса структурой (struct). В той степени, насколько это касается C#, структура является по сути специальным типом класса, который представляется как значение, а не как ссылка. Накладные расходы, связанные с созданием экземпляров структур и их разрушением при завершении с ними работы меньше, чем связанные с созданием экземпляров и разрушением классов. Однако C# ограничивает свойства структур. В частности, нельзя выводить классы или другие структуры из структур. Причина этого состоит в том, что структуры предназначены для использования в качестве динамичных, простых объектов, для которых наследование в действительности не подходит. Фактически все предопределенные классы в C# — int, long, float, double являются на самом деле структурами .NET, вот почему на них можно вызывать такие методы, как ToString(). Тип данных string является, однако, ссылочным типом и поэтому в действительности является классом.

Операторы

Необходимо сказать несколько слов об операторах в C#, так как они действуют несколько иным образом по сравнению с операторами VB, и это может внести путаницу, если программист привык работать с VB. В VB существует на самом деле два типа операторов:

□ Оператор присваивания =, который присваивает значения переменным.

□ Все другие операторы: +, -, * и /, каждый из которых возвращает какое-то значение.

Здесь существует важное различие в том, что ни один из операторов, кроме =, не имеет никакого эффекта в смысле изменения какого-либо значения. Со своей стороны, = присваивает значение, но ничего не возвращает. Не существует оператора, который делает и то и другое.

В C# такого разделения просто не существует. Правило в C# гласит, что все операторы возвращают значение, а некоторые операторы имеют также дополнительный побочный эффект, присваивая некоторое значение переменной. Фактически мы уже видели пример этого, когда рассматривали оператор сложения-присваивания +=:

int А = 5, В = 15;

А += В; // выполняет арифметическую операцию

// и присваивает А результат (20)

Таким образом, += возвращает, а также присваивает значение. Он возвращает новое значение, которое было присвоено. В связи с этим можно на самом деле написать:

int А = 5, B = 15;

int C = (А+=В);

Это в результате приведет в тому, что и А, и С будет присвоено значение 20. Оператор присваивания = также возвращает значение, которое было присвоено переменной с левой стороны выражения. Это означает, что можно записать код следующим образом:

С = (А = В);

Этот код задает А равным значению В, а затем то же самое значение присваивает С. Можно также написать эту инструкцию более просто:

С = А = В;

Обычное использование такого синтаксиса состоит в вычислении некоторого условия внутри инструкции if и одновременном задании результата этого условия в виде переменой типа bool (эквивалент в C# для Boolean из VB), чтобы можно было использовать это значение позже:

// предположим, что X и Y — переменные, которые были инициализированы.

bool В;

if (В = (X==Y)) DoSomething();

Этот код выглядит пугающим на первый взгляд, но он вполне логичен. Давайте разберем его. Прежде всего компьютер проверяет условие X == Y. В зависимости от того, содержат ли X и Y одинаковые данные, оно возвратит true или false и это значение будет присвоено переменной В. Однако поскольку оператор присваивания также возвращает значение, которое было только что присвоено все выражение В = (X==Y) также будет возвращать то же самое значение (true или false). Это возвращаемое значение затем используется предложением if для определения, нужно ли выполнять условную инструкцию DoSometning(). В результате этого кода условие X == Y проверяется для выяснения, должны ли выполняться условные инструкции, и в то же самое время результаты этой проверки сохраняются в переменной В.

Тернарный оператор

В этом приложении нет места для обсуждения всех доступных в C# операторов. Они подробно рассмотрены в главах 3-6. Однако упомянем тернарный оператор (известный также как условный оператор), так как он имеет очень необычный синтаксис. Тернарный оператор формируется из двух символов — ? и :. Он имеет три параметра и на самом деле эквивалентен инструкции If в VB. Синтаксически используется следующие образом:

// В, X и Y являются некоторыми переменными или выражениями.

// В является Boolean.

B ? X : Y

и работает так: оценивается первое выражение, которое расположено перед символом ?, если оно оценивается как true, то возвращается результат второго выражение но если оно оценивается как false, то вместо этого возвращается результат третьего выражения. Это предоставляет предельно компактный синтаксис для условного задания значения переменной. Например, можно написать:

int Z = (Х==Y) ? 5 : 8;

что будет иметь такой же результат, как и следующие вычисления:

int Z;

if (X==Y) Z = 5;

else Z = 8;

Заключение

В этом приложении было представлено краткое введение в C# с точки зрения сравнения его с Visual Basic. Мы обнаружили довольно мало различий в синтаксисе. В целом синтаксис C# позволяет выразить большинство инструкций более компактным образом. Также можно заметить большое сходство между языками, например в их использовании классов (или модулей классов в VB), типов данных значений, ссылочных типов данных и многих синтаксических структур. Однако C# поддерживает также многие мощные свойства, в частности связанные с наследованием и классическим объектно-ориентированным программированием, которые недоступны в VB.

Выполнение перехода от VВ к C# требует специальной подготовки, но стоит того, так как методология C# позволяет не только кодировать приложения, которые можно сделать в VB но также множество других приложений которые было бы трудно или невозможно создать в VB хорошо структурированными и легко поддерживаемыми. При использовании C# можно также получить дополнительный бонус среды выполнения .NET и всех связанных с этим преимуществ.

Приложeниe D

Параметры компиляции C#

Это приложение перечисляет различные параметры компиляции C#, которые можно применять, если необходимо компилировать проекты C#, не используя возможности Visual Studio.NET, или если необходимо выполнить операции компилятора, не поддерживаемые Visual Studio.NET. Они организованы как последовательность таблиц, составленных согласно категориям.

Первая таблица показывает различные форматы файлов, которые может выводить компилятор:

Параметр

Назначение

/doc:<имя файла>

Обрабатывает комментарии документации XML (помеченные тремя слэшами — ///) и выводит в указанный файл XML.

/nooutput

Компилирует код, но не создает файла вывода; полезно для отладочных целей, так как консоль будет показывать предупреждения и ошибки.

/out:<имя файла>

Определяет имя файла вывода. Если оно не указано, то компилятор создает стандартный файл .exe с тем же именем, что и у исходного файла (кроме расширения).

/target:<option> /t:<option>

Определяет формат файла вывода в одном из четырех вариантов: exe: создает стандартный исполняемый файл (задание по умолчанию). library: создает код библиотеки (DLL). module: создает модуль кода (сборку без манифеста), который позже добавляется к сборке (с помощью /addmodule). winexe: создает исполняемый файл для Windows.

Если только не определен параметр /target:module, компилятор будет добавлять манифест в создаваемый ЕХЕ файл (или первый файл DLL, если ЕХЕ не создается). Отметим, что /target можно сократить до /t.

Следующая таблица объясняет параметр командной строки для определения оптимизации компилятора.

Параметр

Назначение

/optimize<+ | -> /о<+ | ->

Включает или выключает оптимизацию, выполняемую компилятором для создания более короткого, быстрого и эффективного вывода. Отключено по умолчанию. Чтобы включить, применяйте синтаксис: /optimize или /optimize+ Чтобы выключить, применяйте синтаксис: /optimize-

Следующая таблица описывает параметры, которые используются, когда создают и ссылаются на сборки .NET.

Параметр

Назначение

/addmodule:<модуль>

Определяет один или несколько модулей, которые будут включены в указанную сборку. Если модулей более одного, то они разделяются с помощью точки с запятой. Этот параметр недоступен в Visual Studio.NET

/nostdlib<+ | ->

Определяет, нужно или нет импортировать стандартную библиотеку (mscorlib.dll), которая импортируется по умолчанию. Если желательно реализовать свое собственное пространство имен и классы System, то компилятор не будет загружать стандартную библиотеку. Синтаксис для этого выглядит следующим образом: /nostdlib или /nostdlib+ Синтаксис для импортирования следующий: /nostdlib-

/reference:<сборка> /r:<сборка>

Импортирует метаданные из файла сборки. Можно определить полный путь доступа к сборке, или определенный везде с помощью переменной окружения PATH, либо относительный путь начинающийся в текущем проекте. Если имеется больше одного файла, они разделяются посредством точки с запятой.

Следующая таблица объясняет параметры, которые применяются при отладке и контроле ошибок.

Параметр

Назначение

/bugreport <имя файла>

Создает указанный файл, который содержит всю информацию об ошибках, выданную компилятором. Содержимое файла включает: копию всего исходного кода, листинг параметров компилятора, информацию о версии компилятора, операционной системе и т.д., всю выдачу компилятора, описание проблемы и возможное решение (по желанию). Эта возможность недоступна в Visual Studio.NET.

/checked<+ | ->

Определяет, даст ли превышение над заданным значением целого числа ошибку времени выполнения. Это применимо только к коду вне области действия блоков checked и unchecked. Отключено по умолчанию. Синтаксис для контроля переполнения следующий: /checked или /checked+. Чтобы отключить контроль переполнения, используйте следующий синтаксис: /checked-

/debug<+ | -> /debug:<option>

Создает информацию отладки. Чтобы включить, используйте синтаксис: /debug или /debug+. Чтобы отключить, используйте: /debug-. Отладка отключена по умолчанию. Если определить, что должна выводиться информация отладки, то имеются две возможности в отношении типа создаваемой информации отладки: /debug:full: разрешает соединение отладчика с операционной системой. /Debug:pdbonly: разрешает отладку исходного кода, когда программа запускается в отладчике, но будет выводить только ассемблерный код, когда выполняющаяся программа присоединяется к отладчику.

/fullpaths

Определяет полный путь доступа к файлу, содержащему ошибку. Эта возможность недоступна в Visual Studio.NET.

/nowarn:<number>

Подавляет способность компилятора создавать специальные предупреждения. Параметр number определяет, какой номер предупреждения подавить. Если определено более одного, то они разделяются запятыми. Это параметр недоступен в Visual Studio.NET.

/warn:<option> /w:<option>

Задает минимальный уровень предупреждений, который желательно выводить. Параметр option показывает: 0: Подавление всех предупреждений. 1: Вывод только серьезных предупреждений. 2: Вывод серьезных предупреждений и предупреждений среднего уровня. 3: Вывод серьезных предупреждений, предупреждений среднего и низкого уровня. 4: Вывод всех сообщений, включая информационные предупреждения

/warnaserror<+ | ->

Интерпретирует все предупреждения как ошибки. Чтобы включить, используйте синтаксис: /warnaserror или /warnaserror+. Чтобы отключить, используйте синтаксис: /warnaserror-. Отключено по умолчанию.

Следующая таблица показывает, как задавать директивы препроцессора:

Параметр

Назначение

/define:<name> /d:<name>

Определяет символ препроцессора, заданный с помощью <name>.

Эта таблица объясняет параметры, связанные с включением внешних ресурсов:

Параметр

Назначение

/linkresourсе:<имя файла> /linkres:<имя файла>

Создает связь с указанным ресурсом .NET. Двумя необязательными дополнительными параметрами (разделенными запятыми) являются: identifier: логическое имя ресурса; имя применяется для загрузки ресурса (по умолчанию используется имя файла), mimetype: строка, представляющая тип среды ресурса (по умолчанию используется none). Эта возможность недоступна в Visual Studio.NET.

/resource:<имя файла> /res:<имя файла>

Вставляет определенный .NET ресурс в файл вывода. Двумя дополнительными необязательными параметрами (разделенными запятыми) являются: identifier: логическое имя ресурса; имя используется для загрузки ресурса (по умолчанию используется имя файла), mimetype: строка представляющая тип среды ресурса (по умолчанию none).

/win32icon:<имя файла>

Вставляет указанный файл пиктограммы Win32 (.ico) в файл вывода.

/win32res:<имя файла>

Вставляет указанный файл ресурса Win32 (.res) в файл вывода. Этот параметр недоступен в Visual Studio.NET.

Заключительная таблица перечисляет смешанные параметры компилятора.

Параметр

Назначение

@<имя файла>

Указывает файл, содержащий все параметры компилятора и исходные файлы, которые будут обрабатываться компилятором, как если бы они вводились в командной строке.

/baseaddress:<address>

Указывает предпочтительный базовый адрес для загрузки DLL. Значение <address> может быть десятичным, шестнадцатеричным или восьмеричным.

/codepage:<id>

Определяет кодовую страницу (значение, передаваемое как параметр <id>) для использования при компиляции всех файлов исходного кода. Вводите этот параметр, если в файлах C# применяется множество символов, не используемых по умолчанию в данной системе. Этот параметр недоступен в Visual Studio.NET.

/help /?

Передает параметры компилятора на стандартный вывод. Этот параметр недоступен в Visual Studio.NET.

/incremental<+ | -> /incr<+ | ->

Разрешает выполнять инкрементную компиляцию файлов исходного кода, которая компилирует только те функции, которые были изменены с момента предыдущей компиляции. Информация о состоянии предыдущей компиляции хранится в двух файлах — .dbg (или .pdb, если был определен параметр /debug) для хранения информации отладки и .incr для хранения информации о состоянии. Чтобы включить параметр, используйте синтаксис: /incremental или /incremental+. Чтобы отключить, используйте синтаксис: /incremental-. Этот параметр отключен по умолчанию.

/main:<class>

Определяет расположение метода Main(), если в исходном коде существует более одного метода с таким именем.

/nologo

Подавляет вывод заголовочной информации компилятора. Этот параметр недоступен в Visual Studio.NET.

/recurce: <dir\file>

Поиск подкаталогов для исходного файла с целью компиляции. Имеются два параметра: dir (необязательный): каталог или подкаталог, из которого начинается поиск. Если не определен, то это каталог текущего проекта. file: файл или файлы для поиска. Можно использовать метасимволы.

/unsafe

Разрешает компиляцию кода, который использует ключевое слово unsafe.

C# Сегодня

Статья "Программное соединение событий в C#" взята из базы знаний на сайте C# Today www.csharptoday.com издательства Wrox. Код, используемый в статье, можно загрузить вместе с кодом для всей книги со страницы Professional C# на Wrox.com.

Программное соединение событий в C#

Мэттью Рейнольдс

Одним из наиболее мощных свойств .NET является возможность создания динамических форм для приложений Windows. Известные и раньше, сегодня они очень легко создаются в .NET. Это позволяет получать объекты, производные из System.Windows.Forms.Control, непосредственно во время выполнения, и использовать их точно таким же образом, как если бы они были созданы проектировщиком форм. Динамические элементы управления могут использоваться для настройки интерфейса пользователя приложения в зависимости от некоторой информации о среде выполнения, например административной утилиты базы данных, где кнопка динамически добавляется в утилиту для каждой таблицы, содержащейся в базе данных. Немного сложным моментом этой темы является соединение обработчиков событий с элементами управления. В этой статье показано, как динамически создавать элементы управления в C# и соединять методы с событиями элементов управления.

Создание проекта

Давайте создадим новый проект Visual C# — Windows Application и назовем его DynamicButtons.

Прежде всего необходимо скомпоновать базовую форму, которая выглядит следующим образом:

Пусть кнопка будет называться cmdCreateButtons, а текстовое поле — txtLog. Убедитесь, что в txtLog свойство Multiline задано как True, а свойство ScrollBars — как Vertical.

Когда будет нажата cmdCreateButtons, мы добавим к форме шесть кнопок, расположенных в пустом пространстве справа от txtLog. В то время как обработчики событий конфигурируются для новых кнопок, определим какая кнопка инициирует вызов. Здесь нужно, чтобы все кнопки имели дополнительные целые свойства с именем ID, которые при создании пронумерованы от 1 до 6.

Одной из отличительных черт .NET, к которой разработчикам VB необходимо привыкнуть, является идея наследования существующих классов из Framework (Среды разработки) и их расширение. Эта техника является очень эффективной. В данном случае необходимо создать новый класс с именем DynamicButton и наследовать его из System.Windows.Forms.Button. Это означает, что наш новый класс будет обладать всей функциональностью обычного элемента управления Button, но при этом иметь и другие свойства, которые нам понадобятся, в частности новое свойство с именем DynamicID. Так как этот класс является производным из Button, он выполняет все действия, присущие элементу управления кнопки, т. е. реагирует на нажатия, может быть помещен в форму и т.д.

Более того, действительно важный аспект наследования состоит в том, что любой знающий, как использовать Button, способен также применять DynamicButton. Однако необязательно знать, как использовать расширенную функциональность, по большей части ее можно применять как обычную кнопку. Вы также вправе вывести свои собственные классы из DynamicButton и добавить свою собственную функциональность. Итак, создадим новый класс с именем DynamicButton и добавим следующий код:

namespace DynamicButton {

using System;

public class DynamicButton : System.Windows.Forms.Button {

public int DynamicId;

public DynamicButton() {

}

}

}

Теперь есть новый класс, который ведет себя так же, как кнопка, но имеет целое свойство с именем DynamicId.

Фактически мы хотим добавить кнопке функциональность, в частности предоставить ID везде, где создается кнопка, и задать текст по умолчанию для отражения этого ID:

public DynamicButton(int newId) {

// задать ID ...

DynamicId = newId;

// задать изображение ...

Text = "Dynamic Button " + DynamicId;

}

Далее можно перейти к cозданию кнопок

Создание кнопок

Чтобы создать кнопки, добавим код обработчика для кнопки cmdCreateButtons. Это относительно простой код, но выглядит немного великоватым, так как нам необходимо самостоятельно управлять компоновкой новых кнопок для отслеживания координат кнопки при ее добавлении.

Первая часть обработчика задает цикл:

protected void cmdCreateButtons_Click (object sender, System.EventArgs e) {

// определить, где должны располагаться новые кнопки ...

int spacing = 8;

int у = txtLog.Top;

int width = this.Width - txtLog.Right - (3 * spacing);

int height = 25;

// цикл создания новыx кнопок

int n = 0;

for (n = 0; n < 6; n++) {

Здесь задано, что высота кнопки равна 25 и у начинается в той же точке, что и вершина txtLog. Мы также задали, что spacing равно 8, это значение используется для разделения кнопок.

Чтобы создать кнопку, делаем так:

// создать новую кнопку ...

DynamicButton newButton = new DynamicButton(n);

Отметим, как значение n передается в конструктор в качестве DynamicId кнопки. Следующим шагом является позиционирование кнопки на форме:

newButton.Left = txtLog.Right + spacing;

newButton.Top = y;

newButton.Width = width;

newButton.Height = height;

Сложность здесь заключается в определении ширины кнопки. Мы хотим, чтобы она находилась на одинаковом расстоянии между правым краем txtLog и правым краем самой формы.

Наконец, мы добавляем кнопку в массив элементов управления и перемещаем у вниз в положение следующей кнопки:

// добавление кнопки в форму ...

this.Controls.Add(newButton);

// следующая ...

у += (height + spacing);

}

}

В результате будет получено изображение:

Кнопки созданы, но пока они ничего не делают. Для того чтобы они работали, необходимо присоединить обработчики событий.

Присоединение обработчиков событий

В .NET существует небольшое различие в способах использования термина делегат. Делегат определяется с помощью ключевого слова Delegate, описывающего сигнатуру метода, который будет использоваться в делегате. Сигнатура метода включает комбинацию параметров и возвращаемый тип. Поэтому метод, который получает один параметр Object и возвращает String, будет иметь сигнатуру, отличную от сигнатуры метода, получающего параметр Hashtable и возвращающего String.

Это выглядит запутанным, потому что вызываемый метод также называется делегатом. В этой статье делегат будет означать определение делегата с помощью ключевого слова Delegate. Делегируемый метод будет обозначать метод, который реализуется в результате вызова делегата.

Для присоединения обработчика событий необходимо создать делегируемый метод со следующей сигнатурой:

void DelegateMethod(Object sender, System.EventArgs e)

Здесь важно то, что имя создаваемого метода является полностью произвольным, т.е. метод может иметь любое имя, какое мы захотим использовать. До тех пор пока создается метод, который не имеет возвращаемого значения, но получает System.Object, а затем объект System.EventArgs,— все будет нормально. Как можно понять, порядок параметров влияет на сигнатуру метода. Если изменить порядок параметров, метод не будет иметь требуемую сигнатуру для делегируемого метода.

Все обработчики событий в множестве элементов управления Windows.Forms соответствуют этой модели, значит, возможно создание достаточно мощных обработчиков событий. Можно было бы при желании сделать так, чтобы каждое событие каждого элемента управления на форме проходило через один и тот же делегируемый метод. Фактически делегируемый метод вызывается для любого объекта из любого другого объекта. Поэтому элемент управления кнопки объекта формы, называемого MyDialog, может вызывать метод для объекта формы, называемого MyDialogOwner.

Параметр sender предоставляет ссылку на элемент управления, который порождает событие. Чтобы добраться до свойства DynamicId, необходимо преобразовать его в тип DynamicButton. Параметр е определяет некоторые специальные аргументы события. Мы не собираемся это здесь рассматривать, но для некоторых событий в данных аргументах содержится дополнительная информация о том, что вызывает событие. Вот обработчик нажатия кнопки:

// DynamicButton_Click ...

protected void DynamicButton_Click(object sender, System.EventArgs e) {

// преобразовать sender в button ...

DynamicButton button = (DynamicButton)sender;

// msgbox ...

AddToLog("Clicked" + button.DynamicId);

}

В этом методе нет ничего необычного. Соединим его с событием Click нашей динамической кнопки. Непосредственно перед тем как вызывать Controls.Add, можно добавить обработчик:

// соединить обработчик ...

newButton.Click += new System.EventHandler(this.DynamicButton_Click);

// добавить кнопку к форме ...

this.Controls.Add(newButton);

Теперь, когда возникает связанное с кнопкой событие Click, рабочая среда Framework просматривает список делегированных методов, соединенных с этим событием, инициируя каждый из них по очереди.

Важная деталь — в .NET события могут иметь более одного обработчика. Это означает, что если создать другой делегированный метод, соединить его с помощью нового экземпляра System.EventHandler, то оба метода будут вызываться в момент порождения события. Конечно, методы не вызываются одновременно, так как фактически получится мультипоточное приложение, а это не то, что хотелось бы случайно получить.

Мы не определили, как выглядит наша функция AddToLog,— сделаем это сейчас:

// AddToLog — обновляет представление журнала ...

private void AddToLog(String buf) {

// обновляет элемент управления журнала ...

txtLog.Text = (buf + "\r\n" + txtLog.Text);

}

Теперь давайте попробуем выполнить приложение и понажимать на кнопки:

Другие обработчики

Обработчики, которые отвечают на другие события в DynamicButton, могут быть добавлены аналогичным образом. Прежде всего для каждого из них создается метод:

// DynamicButton_Enter ...

protected void DynamicButton_Enter(object sender, System.EventArgs e) {

// преобразовать sender в button ...

DynamicButton button = (DynamicButton)sender;

// msgbox ...

AddToLog("Enter " + button.DynamicId);

}

// DynamicButton_Leave ...

protected void DynamicButton_Leave(object sender, System.EventArgs e) {

// преобразовать sender в button ...

DynamicButton button = (DynamicButton)sender;

// msgbox ...

AddToLog("Left " + button.DynamicId);

}

Затем можно добавить обработчики для каждого из них:

// соединить обработчик

newButton.Click +=

new System.EventHandler(this.DynamicButton_Click);

newButton.MouseEnter +=

new System.EventHandler(this.DynamicButton_Enter);

newButton.MouseLeave +=

new System.EventHandler(this.DynamicButton_Leave);

Теперь, если снова выполнить приложение, в журнал будут добавляться сообщения по мере перемещения по кнопкам.

Другой пример

Разобрав пример создания динамических элементов управления, давайте посмотрим, как эта техника может использоваться в приложениях.

Динамические элементы управления можно использовать для настройки интерфейса пользователя приложения в зависимости от некоторых данных среды выполнения. Классическим примером этого является добавление новых возможностей в панель инструментов, когда в каталог приложения вносятся новые дополнительные средства (plug-ins) или модули. Например, установка Adobe Acrobat на компьютере может автоматически добавлять в панель инструментов Word кнопку для создания документа Acrobat.

Другим примером может быть утилита администрирования базы данных. Когда утилита соединяется с базой данных, в интерфейс пользователя должна быть добавлена кнопка, представляющая каждую имеющуюся внутри базы данных таблицу. Или можно создать множество элементов управления текстовых полей с именами всех файлов XML, содержащихся в определенном каталоге, и т.д.

В этом примере мы собираемся создать приложение, загружающее с диска сборку, просматривающее сборку в поисках типов данных, которые наследуют от System.Windows.Forms.Control, и выводящее кнопку в форме для каждого найденного типа. Нажатие на кнопку будет вызывать экземпляр элемента управления и выводить его в форму.

Создание проекта

Создадим новый проект Visual C# — Windows Application и назовем его ControlLoader. В самом начале мы не будем размещать в новой форме никаких элементов управления, но можем изменить свойство Text на что-нибудь типа "Control Container".

Итак, добавим следующие члены в форму:

public class Form1 : System.Windows.Forms.Form {

// члены ...

private ArrayList _buttons = new ArrayList();

private int _nextY = ButtonSpacing;

private Control _containedControl;

// константы ...

const int ButtonHeight = 25;

const int ButtonSpacint = 5;

const int ButtonWidth = 200;

Мы имеем список кнопок, которые добавляются в ArrayList с именем _buttons. При добавлении каждой кнопки к форме необходимо разместить ее в правильной у-координате, т.е. _nextY. Рассмотрим только один элемент управления в конкретный момент, который будет содержаться в _containedControl. Наконец, мы используем метрику, которая описывает компоновку кнопок, и она задается тремя константами внизу.

Сами кнопки будут создаваться из нового класса, производного от System.Windows.Forms.Button. Этот новый класс называется TypeButton и имеет дополнительное свойство ControlType, которое содержит объект System.Type. Этот объект Type представляет элемент управления в загружаемой сборке. Создадим новый класс с именем TypeButton и добавим ссылку на пространство имен System.Windows.Forms.

using System;

using System.Windows.Forms;

Затем добавим код:

public class TypeButton System.Windows.Forms.Button {

public Type _controlType;

public TypeButton() {

}

// ControlType — получить или задать свойство ...

public Type ControlType {

get {

return _controlType;

}

set {

_controlType = value;

this.Text = _controlType.FullName;

}

}

}

Как можно видеть, задавая свойство ControlType, мы изменяем текст кнопки, чтобы он стал полным названием типа.

Метод, который нужен для создания TypeButton, создает экземпляр элемента управления. Добавим этот метод, использующий System.Activator для создания экземпляра класса и преобразующий его в System.Windows.Forms.Control:

// CreateInstance — создание экземпляра типа данных ...

public Control CreateInstance() {

// возвращает экземпляр нового элемента управления ...

return (Control)Activator.CreateInstance(ControlType);

}

Добавление кнопок

Для первого проверочного запуска добавим TypeButton, который представляет экземпляр элемента управления System.Windows.Forms.DataGrid. Это позволит протестировать логику того, что делается, не вдаваясь в дополнительные трудности, связанные с загрузкой сборки и просмотром типов данных.

Метод Form1.AddType будет получать объект System.Type и добавлять новый объект TypeButton в форму. Во-первых, необходимо создать экземпляр TypeButton и задать его свойство ControlType новым типом:

// AddType - добавить кнопку типа в компоновку ...

public void AddType(Type ControlType) {

// первое: создать новую кнопку типа ...

TypeButton button = new TypeButton();

button.ControlType = ControlType;

Во-вторых, нужно добавить TypeButton в ArrayList, который содержит список кнопок:

// второе: добавить эту кнопку в массив

_buttons.Add(button);

После этого можно поместить в форму кнопку и добавить ее в список элементов управления формы:

// теперь разместим кнопку

button.Left = ButtonSpacing;

button.Width = ButtonWidth;

button.Top = _nextY;

button.Height = ButtonHeight;

// настроить следующее значение у ...

_nextY += (ButtonHeight + ButtonSpacing);

// вывести кнопку ...

this.Controls.Add(button);

Наконец, необходимо присоединить событие click (нажатие) кнопки таким образом, чтобы мы могли иметь экземпляр элемента управления, который он представляет:

// затем присоединяем обработчик события ...

button.Click += new EventHandler(this.ButtonClick);

}

Пока еще мы не создали ButtonClick, — сделаем это сейчас:

// ButtonClick — вызывается всякий раз при нажатии кнопки ...

protected void ButtonClick(object sender, System.EventArgs e) {

// преобразовать sender в кнопку типа ...

TypeButton button = (TypeButton)sender;

Нашей первой задачей является преобразование sender в TypeButton. Это позволит использовать CreateInstance для создания элемента управления. Если мы уже имеем элемент управления, необходимо сначала удалить его из списка элементов управления формы:

// если уже имеется содержащийся элемент управления, удалим его ...

if (_containedControl != null)

Controls.Remove(_containedControl);

Создаем элемент управления:

// создать экземпляр нового элемента управления...

_containedControl = button.CreateInstance();

Наконец, мы можем поместить элемент управления в форму.

// поместить элемент управления на форме ...

_containedControl.Left = ButtonWidth + (3 * ButtonSpacing);

_containedControl.Top = ButtonSpacing;

_containedControl.Width = this.Width - _containedControl.Left - (4 * ButtonSpacing);

_containedControl.Height = this.Height - (8 * ButtonSpacing);

this.Controls.Add(_containedControl);

}

Тестирование полученного кода

Прежде чем можно будет увидеть окончательный вариант сделанного, необходимо добавить кнопку в форму. Используем элемент управления System Windows.Forms.DataGrid. Добавим следующий код в Form1:

private void Form1_Load(object sender, System.EventArgs e) {

// при загрузке добавить тип кнопки ...

DataGrid grid = new DataGrid();

AddType(grid.GetType());

}

Теперь закончим проект. Нажав на кнопку System.Windows.Forms.DataGrid, увидим:

Загрузка сборки

Завершая этот пример, покажем, как можно загрузить сборку во время выполнения, просмотреть сборку в поисках типов данных, которые являются произвольными из System.Windows.Forms.Control, и добавить кнопки для каждого найденного типа данных.

Чтобы загрузить сборку нам понадобится URL. Добавим следующий код в Form1_Load:

private void Form1_Load(object sender, System.EventArgs e) {

// при загрузке добавить тип кнопки ...

DataGrid grid = new DataGrid();

AddType(grid.GetType());

// найти имя нашей сборки

String filename = this.GetType().Module.Assembly.CodeBase;

Эта строка является обходной техникой для получения сборки, реализующей класс объекта, из которого вызывается код. В данном случае ее получают из URL сборки, содержащей Form1.

Нам нужно при загрузке сборки использовать try…catch, так как существуют процессы, которые могут пойти в этой процедуре неправильно. Мы используем общий метод на Assembly:

// проверить и загрузить сборку ...

try {

// используем LoadFrom ...

Assembly controlAssembly = Assembly.LoadFrom(filename);

После получения сборки проверим в ней типы данных:

// теперь получим список типов данных ...

foreach(Type testType in controlAssembly.GetTypes()) {

Далее хитрость состоит том, чтобы создать экземпляр проверяемого типа данных, но преобразовать его в Control. Так как все это погружено в другой блок try…catch, то при переходе к следующей строке мы знаем, что имеется полностью квалифицированный элемент управления.

// попробуем создать экземпляр элемента управления

// и преобразовать его в элемент управления ...

try {

Control testControl = (Control)Activator.CreateInstance(testType);

Полезный совет. System.Windows.Forms.Form является производным от Control, так как он использует контейнеризацию свойств Control для вывода элементов управления, нарисованных в форме. Если проверить свойство TopLevelControl, оно всегда будет задано при выводе класса из формы.

// нам необходимо убедиться,

// что это не элемент управления "верхнего уровня" ...

if (testControl.TopLevelControl == null) {

// если мы здесь оказались, то это элемент управления ...

AddType(testType);

}

}

Мы можем завершить пример двумя обработчиками исключений:

catch {

// если мы здесь, мы не заботимся об объекте!

}

}

} catch(Exception ее) {

MessageBox.show("The assembly could not be loaded. " + ее.Message);

}

}

Прежде чем это проверить, необходимо поместить в проект другие элементы управления. Создадим первый класс, называемый DemoTextBox, и добавим следующее предложение наследования:

public class DemoTextBox : System.Windows.Forms.TextBox

Теперь создадим другой класс, на этот раз с именем DemoMonthCalendar, и добавим следующее предложение:

public class DemoMonthCalendar : System.Windows.Forms.MonthCalendar

Выполним проект. Должно получиться подобное изображение.

Заключение

Из этой статьи мы узнали, как динамически создавать элементы управления и добавлять их в форму. Мы ввели новый класс, производный от System.Window.Forms.Button, который позволяет добавлять дополнительною функциональность и свойства кнопке. Мы увидели также, как соединить с новыми элементами управления методы, вызываемые при инициировании событий. Наконец, мы проверили возможность просмотра сборки в поисках классов и использовали изученную технику для создания простой утилиты, которая загружается и выводит эти классы по команде пользователя.

Content

C# для профессионалов
Том II

Глава 13 XML

Стандарты W3C

Пространство имен System.Xml

XML 3.0 (MSXML3.DLL) в C#

System.Xml

Чтение и запись XML

XmlTextReader

Проверка

Запись XML

Объектная модель документа в .NET

XPath и XslTransform

XML и ADO.NET

Данные ADO.NET в документе XML

Преобразование документа XML в данные ADO.NET

Запись и чтение DiffGram

Сериализация объектов в XML

Заключение

Глава 14 Операции с файлами и реестром

Управление файловой системой

Классы .NET, представляющие файлы и папки

Класс Path

Пример: файловый браузер

Перемещение, копирование и удаление файлов

Пример FilePropertiesAndMovement

Чтение и запись файлов

Потоки

Чтение и запись двоичных файлов

Класс FileStream

Пример: объект чтения двоичного файла

Чтение и запись текстовых файлов

Класс StreamReader

Класс StreamWriter

Пример: ReadWriteText

Чтение и запись в реестр

Реестр

Классы реестра в .NET

Пример: SelfPlacingWindow

Заключение

Глава 15 Работа с активным каталогом

Архитектура активного каталога

Свойства

Концепции активного каталога

Объекты

Схема

Конфигурация

Домен активного каталога

Контроллер домена

Сайт

Дерево домена

Лес

Глобальный каталог

Репликация

Характеристики данных активного каталога

Схема

Управление активным каталогом

Active Directory Users and Computers

ADSI Edit

ADSI Viewer

Интерфейсы службы активного каталога (ADSI)

Программирование активного каталога

Классы в System.DirectoryServices

Связывание

Протокол

Имя сервера

Номер порта

Известное имя

Имя пользователя

Аутентификация

Связывание с помощью класса DirectoryEntry

Получение записей каталога

Свойства объектов пользователей

Коллекции объектов

Кэш

Обновление записей каталога

Создание новых объектов

Поиск в активном каталоге

Пределы поиска

Поиск объектов пользователей

Интерфейс пользователя

Получение именующего контекста схемы

Получение имен свойств класса пользователя

Поиск объектов User

Заключение

Глава 16 Страницы ASP.NET

Введение в ASP.NET

Управление состоянием в ASP.NET

Формы Web ASP.NET

Серверные элементы управления ASP.NET

Палитра элементов управления

Пример серверного элемента управления

ADO.NET и связывание данных

Модернизация приложения заказа помещения

База данных

Соединение с базой данных

Модификация элемента управления календарем

Запись мероприятий в базу данных

Еще о связывании данных

Вывод данных с помощью шаблонов

Конфигурация приложения

Заключение

Глава 17 Службы Web

SOAP

WSDL

Службы Web

Создание служб Web

Типы данных, доступные для служб Web

Использование служб Web

Расширение примера заказа помещения для проведения мероприятий

Служба Web заказа помещения для проведения мероприятий

Клиент приложения предварительного заказа помещения для проведения мероприятия

Заключение

Глава 18 Специальные элементы управления

Элементы управления пользователя

Простой элемент управления пользователя

Преобразование приложения предварительного заказа мероприятия в элемент управления пользователя

Специальные элементы управления

Конфигурация проекта специального элемента управления

Базовые специальные элементы управления

Производный элемент управления RainbowLabel

Поддержание состояния в специальном элементе управления

Создание композитного специального элемента управления

Элемент управления выборочным опросом

Элемент управления Option

Построитель элемента управления StrawPoll

Стиль StrawPoll

Элемент управления StrawPoll

Добавление обработчика событий

Заключение

Глава 19 Взаимодействие с COM

Сравнение COM и .NET

Принципы работы COM

Недостатки COM

Как работают компоненты .NET

COM или .NET?

Использование компонентов COM в .NET

Диалоговое окно ссылок

Оболочки времени выполнения

TlbImp.exe

Позднее связывание с компонентами COM

Использование элементов управления ActiveX в .NET

AxImp.exe

Ссылка на сборку прокси ActiveX

Размещение элемента управления ActiveX в WinForm

Использование компонентов .NET в COM

RegAsm.exe

TlbExp.exe

Службы вызова платформы

Неуправляемый код и ненадежный код

Доступ к неуправляемому коду

Недостатки PInvoke

Заключение

Главa 20 Службы COM+

Введение

Службы COM+ в ретроспективе

Состав служб COM+

Snap-in служб компонентов

Транзакции COM+

Назначение транзакций

Принципы транзакций

Транзакции в N-звенной архитектуре

Службы COM+ и время жизни объекта

Создание пулов объектов

Оперативная активизация (JIT)

Безопасность

Новые службы COM+

События

Очереди сообщений

Выравнивание нагрузки компонентов

Использование служб COM со сборками .NET

Подготовка сборок .NET для служб COM+

Предоставление атрибутов сборок

Развертывание сборки для служб COM+

Предварительные итоги

Использование транзакций со сборками .NET

Определение транзакционной поддержки

Кодирование транзакций с помощью ContextUtil

Другие полезные методы ContextUtil

Использование пудов объектов со сборками .NET

Атрибут ObjectPooling

Интерфейс ServicedComponent

Использование активизации JIT со сборками .NET

Заключение

Глава 21 Графические возможности GDI+

Основные принципы рисования

GDI и GDI+

Контексты устройств и объект Graphics

Пример: рисование контуров

Рисование фигур с помощью OnPaint

Использование области вырезания

Измерение координат и областей

Point и PointF

Size и SizeF

Rectangle и RectangleF

Region

Замечание об отладке

Изображение прокручиваемых окон

Координаты мировые, страницы и устройства

Цвета

Значения красный-зеленый-синий (RGB)

Именованные цвета

Режимы вывода графики и палитра безопасности

Палитра безопасности

Перья и кисти

Кисти

Перья

Рисование фигур и линий

Вывод изображений

Вопросы, возникающие при манипуляциях с изображениями

Рисование текста

Простой пример с текстом

Шрифты и семейства шрифтов

Пример: перечисление семейств шрифтов

Редактирование текстового документа: пример CapsEditor

Метод Invalidate()

Вычисление размеров объектов и размера документа

OnPaint()

Преобразования координат

Ответ на ввод пользователя

Печать

Заключение

Глава 22 Доступ в Интернет

Класс WebClient

Загрузка файлов

Пример: базовый клиент Web

Выгрузка файлов

Классы WebRequest

Другие свойства WebRequest и WebResponse

Отображение выходных данных в виде страницы HTML

Иерархия классов WebRequest и WebResponse

Служебные классы

URI

Пример вывода страницы

Адреса IP и имена DNS

Классы .NET для адресов IP

Пример: DnsLookup

Протоколы нижнего уровня

Классы нижнего уровня

Заключение

Главa 23 Создание распределенных приложений с помощью .NET Remoting

Что такое .NET Remoting

Web Services Anywhere

CLR Object Remoting

Обзор .NET Remoting

Контексты

Активизация

Атрибуты и свойства

Коммуникация между контекстами

Удаленные объекты, клиенты и серверы

Удаленные объекты

Простой сервер

Простой клиент

Архитектура .NET Remoting

Каналы

Задание свойств канала

Подключаемость канала

Форматтеры

ChannelServices и RemotingContiguration

Сервер для активизированных клиентом объектов

Активизация объектов

URL-приложения

Активация хорошо известных объектов

Активизация объектов, активизированных клиентом

Объекты прокси

Сообщения

Приемники сообщений

Уполномоченный приемник

Приемник серверного контекста

Объектный приемник

Передача объектов в удаленные методы

Направляющие атрибуты

Управление временем жизни

Обновление аренды

Классы, используемые для управления временем жизни

Пример: получение информации об аренде

Изменение используемых по умолчанию конфигураций аренды

Конфигурационные файлы

Конфигурация сервера для хорошо известных объектов

Конфигурация клиента для хорошо известных объектов

Серверная конфигурация для активизированных клиентом объектов

Клиентская конфигурация для активизированных клиентом объектов

Серверный код, использующий конфигурационные файлы

Клиентский код, использующий конфигурационные файлы

Службы времени жизни в конфигурационных файлах

Инструменты для файлов удаленной конфигурации

Приложения хостинга

Хостинг удаленных серверов в ASP.NET

Классы, интерфейсы и SOAPSuds

Интерфейсы

SOAPSuds

Отслеживание служб

Асинхронная удаленная работа

Атрибут OneWay

Удаленное выполнение и события

Удаленный объект

Аргументы событий

Сервер

Приемник событий

Клиент

Выполнение программы

Контексты вызова

Заключение

Глава 24 Службы Windows

Понятие службы

Архитектура

Служебная программа

Управляющий менеджер служб

Служебная управляющая программа

Конфигурационная программа службы

Пространство имен System.ServiceProcess

Создание службы

Библиотека классов, использующая сокеты

Пример TcpClient

Проект Windows Service

Класс ServiceBase

Потоки выполнения и службы

Установка службы

Программы установки

Класс Installer

Классы ServiceProcessInstaller и ServiceInstaller

ServiceInstallerDialog

InstallUtil

Клиент

Мониторинг и управление службой

Консоль управления Microsoft (ММС)

net.exe

sc.exe

Server Explorer

Класс ServiceController

Управление службой

Поиск неисправностей

Интерактивные службы

Регистрация событий

Архитектура регистрации событий

Классы регистрации событий

Добавление регистрации событий

Трассировка

Создание приемника событий

Мониторинг производительности

Классы мониторинга производительности

Построитель счетчиков производительности

Добавление счетчиков производительности

perfmon.exe

Служба счетчика производительности

Свойства служб Windows 2000

Изменения сетевого соединения и события электропитания

Восстановление

Приложения COM+ в роли служб

Заключение

Глава 25 Система безопасности .NET

Система безопасности доступа к коду

Группы кода

Caspol.exe — утилита политики системы безопасности доступа к коду

Полномочия доступа к коду и множества полномочий

Просмотр полномочий сборки

Уровни политики: машина, пользователь и предприятие

Поддержка безопасности в .NET

Требуемые полномочия

Запрашиваемые полномочия

Неявное полномочие

Отказ от полномочий

Заявляемые полномочия

Создание полномочий доступа к коду

Декларативная безопасность

Система безопасности на основе ролей

Принципал

Принципал Windows

Роли

Система безопасности на основе декларативной роли

Управление политикой системы безопасности

Конфигурационный файл системы безопасности

Простой пример

Управление группами кода и полномочиями

Включение и выключение системы безопасности

Восстановление политики системы безопасности

Создание группы кода

Удаление группы кода

Изменение полномочий группы кода

Создание и применение множеств полномочий

Распространение кода с помощью строгого имени

Распространение кода с помощью сертификатов

Управление зонами

Заключение

Пpиложeние A C# для разработчиков C++

Введение

Соглашения в этом приложении

Терминология

Сравнение C# и C++

Различия

Сходства

Новые свойства

Новые свойства базовых классов

Неподдерживаемые свойства

Пример Hello World

Инструкции #include

Пространства имен

Точка входа: Main() и main()

Вывод сообщения

Сравнение свойств

Архитектура программы

Программные объекты

Файловая структура

Точка входа программы

Синтаксис языка

Опережающие объявления

Отсутствие разделения определения и объявления

Поток выполнения программы

if…else

while и do…while

switch

foreach

Переменные

Базовые типы данных

Базовые типы данных как объекты

Преобразования базовых типов данных

Проверяемое (checked) преобразование типов данных

Строки

Последовательности кодирования

Типы значений и ссылочные типы

Инициализация переменных

Упаковка

Управление памятью

Оператор new

Методы

Параметры методов

Перезагрузка методов

Свойства

Операторы

Оператор присваивания (=)

this

new

Классы и структуры

Классы

Определение класса

Инициализация полей членов

Конструкторы

Статические конструкторы

Конструкторы по умолчанию

Списки инициализации конструктора

Деструкторы

Наследование

Виртуальные и невиртуальные функции

Структуры

Константы

Константы, ассоциированные с классом (статические константы)

Константы экземпляра

Перезагрузка операторов

Индексаторы

Определенные пользователем преобразования типов данных

Массивы

Одномерные массивы

Многомерные массивы

Проверка границ

Изменение размера массивов

Перечисления

Исключения

Указатели и небезопасный код

Фиксация донных в куче

Объявление массивов в стеке

Интерфейсы

Делегаты

События

Атрибуты

Директивы препроцессора

Пpиложение B C# для разработчиков Java

Основы

Идентификаторы

Стандарты именования

Ключевые слова

Входные и выходные данные

Компиляция

Пространства имен

Создание и добавление библиотек при компиляции

Обнаружение и разрешение

Строгие имена и глобальный кэш

Типы данных

Простые типы

Типы перечислений

Структуры

Ссылочные типы

Операторы

Присваивание

Сравнение

Операторы равенства aрифметические, условные, побитовые, битового дополнения и сдвига

Преобразование типов

Перезагрузка

sizeof и typeof

Делегаты

Подробно о классах

Модификаторы

Конструкторы

Методы

Свойства и индексаторы

События

Исключения

Условная компиляция

Вопросы безопасности

Заключение

Приложение C C# для разработчиков VB6

Различия между C# и VB

Классы

Компиляция

Базовые классы .NET

Соглашения

Пример: Форма для извлечения квадратного корня

Версия SquareRoot на VB

Версия SquareRoot на C#

Базовый синтаксис

С# требует, чтобы все переменные были объявлены

Комментарии

Разделение и группировка инструкций

Использование заглавных букв

Методы

Переменные

Объявления

Присваивание значений переменным

Классы

Инструкции If

Вычисление квадратного корня: еще один метод класса

Строки

Дополнительный код в C#

Что происходит при выполнении программы

Код C# для оставшейся части программы

Пространства имен

Инструкция using

Определение класса: наследование

Точка входа в программу

Создание экземпляров классов

Классы С#

Вход в цикл сообщений

Класс формы SquareRoot

Подводя итог

Пример: Employees и Managers

Модуль класса Employee в VB

Класс Employee в C#

Конструктор Employee

Свойства класса Employee

Методы класса Employee

Статические члены

Наследование

Наследование от класса Employee

Класс Manager

Переопределение метода

Конструкторы класса Manager

Перезагрузка методов

Использование классов Employee и Manager

Ссылки на производные классы

Массивы объектов

Цикл for

Другие свойства C#

Типы данных

Типы данных значений и ссылочные типы данных

Операторы

Тернарный оператор

Заключение

Приложeниe D Параметры компиляции C#

C# Сегодня

Программное соединение событий в C#

Другой пример

Загрузка сборки

Заключение