Создание XML

65

Как уже упоминалось, функциональное конструирование, предлагаемое LINQ to XML, позволяет очень легко создавать дерево XML, особенно если сравнить это с W3C DOM API. Рассмотрим теперь создание каждого из основных классов в LINQ to XML.

Поскольку новый API-интерфейс ориентирован на элементы, и именно они будут создаваться большей частью, сначала описано создание элементов с помощью класса XElement. Затем будут показаны остальные классы XML в алфавитном порядке.

Создание элементов с помощью XElement

Для начала следует запомнить, что в этом новом API-интерфейсе класс XElement является одним из наиболее часто используемых. Рассмотрим создание экземпляра XElement. Класс XElement имеет несколько конструкторов, но ниже показаны два из них:

XElement.XElement(XName name, object content); 
XElement.XElement(XName name, params object[] content);

Первый конструктор представляет простейший случай, когда элемент имеет текстовое значение и не имеет дочерних узлов. Этот случай демонстрируется ниже:

XElement firstName = new XElement("FirstName","Alex");
Console.WriteLine((string)firstName);

Первый аргумент конструктора — объект XName. Как упоминалось ранее, объект XName создается неявным преобразованием входного параметра string в XName. Второй аргумент — единственный объект, представляющий содержимое элемента. В данном случае содержимое — это string со значением "Alex". API-интерфейс "на лету" преобразует этот строковый литерал "Alex" в объект XText. Обратите внимание, что здесь для получения значения из переменной элемента firstName используется преимущество новых средств извлечения значения узла. То есть элемент должен быть приведен к типу его значения, которым в данном случае является string. Таким образом, будет извлечено значение элемента firstName. И вот результат:

Создание элемента с использованием первого прототипа

Тип данных единственного объекта содержимого очень гибок. Это тип данных объекта содержимого, который контролирует его отношение к элементу, к которому он добавлен. Ниже описаны все допустимые типы данных объектов содержимого, а также то, как они обрабатываются:

Способы обработки созданных типов классом XElement
Тип данных объекта содержимого Способ обработки
string Объект string или строковый литерал автоматически преобразуется в объект XText и с этого момента обрабатывается как XText
XText Этот объект может иметь значение либо string, либо XText. Добавляется как дочерний узел элемента, но трактуется как текстовое содержимое элемента
XCData Этот объект может иметь значение либо string, либо XCData. Добавляется как дочерний узел элемента, но трактуется как CData-содержимое элемента
XElement Этот объект добавляется как дочерний элемент
XAttribute Этот объект добавляется как атрибут
XProcessingInstruction Этот объект добавляется как дочернее содержимое
XComment Этот объект добавляется как дочернее содержимое
IEnumerable Этот объект перечисляется, и обработка типов составляющих его объектов выполняется рекурсивно
null Этот объект игнорируется. Вы можете удивиться, зачем вообще может понадобиться передавать null конструктору элемента, но оказывается, это очень полезно для трансформаций XML
Любой прочий тип Вызывается метод ToString, и результирующее значение трактуется как содержимое string

Помните, что несмотря на то, что значение элемента может быть сохранено как string, как это возможно для любого прочего типа, такого как целое число, благодаря новым средствам извлечения значения, его можно получить в исходном типе. Поэтому, например, если при создании объекта XElement в качестве объекта содержимого указано int, то приведением узла к int получится значение, преобразованное в int. До тех пор пока выполняется приведение к одному из типов, для которых предусмотрена операция приведения, и пока значение элемента может быть преобразовано в тип, к которому выполняется приведение, такая операция представляет простой способ получения значения элемента.

Второй конструктор XElement, показанный выше, подобен первому, за исключением того, что можно предоставить множество объектов для содержимого. Именно это делает функциональное конструирование столь мощным. Примеры использования второго конструктора, где множество объектов содержимого передается конструктору XElement, приведены в статье Интерфейс LINQ to XML API.

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

Первым делом, необходим класс, в котором будут храниться данные. Кроме того, поскольку есть типы Employees, имеет смысл создать перечисление enum для разных типов:

enum EmployeTypes
{
        Programmer = 0,
        Editor
}

class Employee
{
        public string FirstName;
        public string LastName;
        public EmployeTypes EmployeType;
}

Теперь построим массив типа Employee и сгенерируем дерево XML, применяя запрос LINQ для извлечения данных из массива, как показано ниже:

Employee[] emps = new[] { 
                    new Employee { FirstName = "Alex", LastName="Erohin", EmployeType=EmployeTypes.Programmer},
                    new Employee { FirstName="Elena", LastName="Volkova", EmployeType=EmployeTypes.Editor}};

XElement xEmp = 
                new XElement("Employees",
                    emps.Select(p =>
                        new XElement("Employee",
                            new XAttribute("type", p.EmployeType),
                            new XElement("FirstName", p.FirstName),
                            new XElement("LastName", p.LastName))));
Console.WriteLine(xEmp);

В приведенном коде создается массив объектов Employee по имени emps. Затем с помощью операции Select из emps запрашиваются значения. Для каждого значения генерируется элемент Employee, используя члены элемента массива. Так выглядит дерево XML, сгенерированное предыдущим кодом:

Генерация дерева XML запросом LINQ

Только представьте, как это пришлось бы делать в W3C XML DOM API. Впрочем, можно просто взглянуть на код примера из статьи «Интерфейс LINQ to XML API», потому что код создает точно такое же дерево XML.

Создание атрибутов с помощью XAttribute

В отличие от W3C XML DOM API, атрибуты не наследуются от узлов. Атрибут реализованный в LINQ to XML с помощью класса XAttribute, является парой "имя-значение", хранящейся в коллекции объектов XAttribute, которые относятся к объекту XElement. Используя функциональное конструирование, можно создать атрибут и "на лету" добавить его к элементу, как показано ниже:

XElement xEmployee = new XElement("Employee",
                new XAttribute("type", "Programmer"));

Console.WriteLine(xEmployee);

Запуск этого кода даст следующий результат:

Создание атрибута посредством LINQ to XML

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

XElement xEmployee = new XElement("Employee");
XAttribute xAttr = new XAttribute("type", "Programmer");

xEmployee.Add(xAttr);
Console.WriteLine(xEmployee);

Результат получается аналогичным предыдущему.

Обратите внимание на гибкость метода XElement.Add. Он принимает любой объект применяя те же правила для содержимого элемента, что и при создании экземпляра XElement. Замечательно!

Создание комментариев с помощью XComment

Создание комментариев с LINQ to XML тривиально. Комментарии XML реализуются в LINQ to XML с помощью класса XComment. С использованием функционального конструирования можно создать комментарий и добавить его к элементу "на лету", как показано в следующем примере:

XElement xEmployee = new XElement("Employee",
                new XComment("Добавление нового сотрудника"));

Console.WriteLine(xEmployee);

Запуск этого кода даст следующий результат:

Создание комментария посредством LINQ to XML

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

XElement xEmployee = new XElement("Employee");
XComment xCom = new XComment("Добавление нового сотрудника");

xEmployee.Add(xCom);
Console.WriteLine(xEmployee);

Результат получается аналогичным предыдущему.

Создание контейнеров с помощью XContainer

Поскольку XContainer — абстрактный класс, создавать его экземпляры нельзя. Вместо этого понадобится создавать экземпляр одного из его подклассов — XDocument или XElement. Концептуально XContainer — это класс, унаследованный от XNode, который может содержать другие классы-наследники XNode.

Создание объявлений с помощью XDeclaration

В API-интерфейсе LINQ to XML создание объявлений также просто. Объявления XML реализованы здесь классом XDeclaration.

В отличие от большинства других классов LINQ to XML, объявления должны добавляться к XML-документу, а не к элементу. Помните, насколько гибкий конструктор у класса XElement? Любой класс, не спроектированный специально для этого, должен иметь собственный метод ToString, и выведенный им текст должен быть добавлен к элементу в виде текстового содержимого. Поэтому, используя класс XDeclaration, можно по невнимательности добавить к элементу объявление. Однако это не даст ожидаемого результата.

В то время как объявления XML применяются к документу XML в целом и должны быть добавлены к нему, объект XElement также благополучно принимает добавляемый к нему объект XDeclaration. Тем не менее, это не дает результат, который ожидается.

С помощью функционального конструирования можно создать объявление и добавить его к XML-документу "на лету", как показано ниже:

XDocument xDoc = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"),
                new XElement("Employee"));

Console.WriteLine(xDoc);

Этот код производит следующий результат:

Создание XML-объявлния посредством LINQ to XML

Вы заметили, что в выводе нет объявления? Все правильно: метод ToString пропускает его. Однако если во время отладки кода заглянуть в документ в окне слежения, обнаружится, что объявление на месте.

Однако иногда создать объявление одновременно с конструируемым документом невозможно. В этом случае необходимо создать его экземпляр, а затем установить его в качестве значения свойства Declaration документа, как показано ниже:

XDocument xDoc = new XDocument(new XElement("Employee"));
XDeclaration xDeclaration = new XDeclaration("1.0", "UTF-8", "yes");

xDoc.Declaration = xDeclaration;
Console.WriteLine(xDoc);

Результат получается аналогичным предыдущему.

Опять-таки, обратите внимание, что объявление не попадает в вывод при вызове метода ToString документа. Но, как и в предыдущем примере, если просмотреть документ во время отладки кода, оно там обнаружится.

Создание типов документов с помощью XDocumentType

API-интерфейс LINQ to XML делает операцию создания типов документов совершенно безболезненной. Типы XML-документов реализованы LINQ to XML с помощью класса XDocumentType.

В отличие от большинства других классов в LINQ to XML, типы документов предназначены для добавления к XML-документам, а не к элементам. Помните, насколько гибок конструктор класса XElement? Любой класс, который не был специально спроектирован для обработки, должен иметь метод ToString, и полученный текст будет добавлен к элементу в качестве текстового содержимого. Поэтому можно нечаянно добавить тип документа XDocumentType к элементу, но это не даст ожидаемого результата.

Хотя типы документов XML применимы к документу XML в целом, и должны добавляться к документу XML, объект XElement благополучно принимает добавляемый к нему объект XDocumentType. Однако это не дает результата, которого можно было ожидать.

С помощью функционального конструирования можно создавать тип документа и добавлять его к документу XML "на лету", как показано ниже:

XDocument xDoc = new XDocument(new XDocumentType("Employees", null, "Employees.dtd", null),
                new XElement("Employee"));
Console.WriteLine(xDoc);

Запуск этого кода даст следующий результат:

Создание DOCTYPE-объявления посредством LINQ to XML

Однако иногда не удается создать тип документа одновременно с конструированием этого документа. Тогда понадобится сначала создать его экземпляр, а затем добавить к документу, как показано ниже:

XDocument xDoc = new XDocument();
XDocumentType docType = new XDocumentType("Employees", null, "Employees.dtd", null);

xDoc.Add(docType, new XElement("Employee"));
Console.WriteLine(xDoc);

Результат получается аналогичным предыдущему.

Обратите внимание, что в предыдущем коде перед добавлением типа документа не было добавлено ни одного элемента. Если добавить тип документа после добавления любого элемента, сгенерируется следующее исключение:

Unhandled Exception: System.InvalidOperationException: 
This operation would create an incorrectly structured document.

Необработанное исключение: System. InvalidOperationException: 
Эта операция создаст некорректно структурированный документ.
...

Поэтому, если вы собираетесь указать тип документа после создания его экземпляра, удостоверьтесь, что не указали ни одного элемента при создании экземпляра документа. используя функциональное конструирование, или не добавили ни одного элемента перед добавлением типа документа.

Создание документов с помощью XDocument

Хоть это уже повторялось много раз, но стоит это сделать еще раз: в LINQ to XML не обязательно создавать XML-документ только для того, чтобы создать дерево XML или его фрагмент. Однако при необходимости, создание XML-документа с LINQ to XML столь же тривиально. Документы XML реализованы в LINQ to XML в виде класса XDocument:

XDocument xDocument = new XDocument(); 
Console.WriteLine(xDocument);

Этот код не производит никакого вывода, потому что созданный XML-документ пуст. Приведенный пример может быть, чересчур тривиален, поэтому ниже создается документ с помощью всех классов LINQ to XML, которые специально предназначены для добавления к объекту XDocument:

XDocument xDoc = new XDocument(
                new XDeclaration("1.0", "UTF-8", "yes"),
                new XDocumentType("Employees", null, "Employees.dtd", null),
                new XProcessingInstruction("EmployeeCataloger", "out-of-print"),
                new XElement("Employees"));

Console.WriteLine(xDoc);

Инструкция обработки и элемент могут добавляться также и к элементам, но нужно было получить XML-документ с некоторым содержимым. Также добавлена инструкция обработки, так что ее можно будет увидеть в действии:

 более сложный пример создания XML-документа с помощью XDocument

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

Создание имен с помощью XName

Как упоминалось ранее, в LINQ to XML нет необходимости непосредственно создавать имена с объектами XName. Фактически класс XName не имеет общедоступных конструкторов, так что нет способа создавать его экземпляры. Объект XName может быть создан из строки с необязательным пространством имен автоматически при создании объекта XName.

Объект XName состоит из LocalName — строки — и пространства имен, которое представлено в XNamespace. Пример ниже содержит код вызова конструктора XElement, принимающего XName в качестве аргумента:

XNamespace ns = "http://www.professorweb.ru/LINQ";
XElement xEmps = new XElement(ns + "Employee");
Console.WriteLine(xEmps);

В приведенном примере создается экземпляр объекта XElement с передачей ему имени элемента в виде строки, так что объект XName создается с LocalName равным Employee и присваивается свойству Name объекта XElement. Также указано пространство имен. Нажав <Ctrl+F5>, получим следующий результат:

Пример, где автоматически создается объект XName и указано пространство имен

Создание инструкций обработки с помощью XProcessingInstruction

Инструкции обработки никогда ранее не было так легко создавать, как в API-интерфейсе LINQ to XML. Здесь они реализуются классом XProcessingInstruction.

Инструкции обработки можно создавать на уровне документа или элемента. Ниже приведен пример их создания "на лету" в обоих случаях с помощью функционального конструирования:

XDocument xDoc = new XDocument(
        new XProcessingInstruction("EployeCataloger", "out-of-print"),
        new XElement("Employees",
            new XElement("Employee",
                new XProcessingInstruction("EmployeeDeleter", "delete"),
                new XElement("FirstName", "Alex"),
                new XElement("LastName", "Erohin"))));
Console.WriteLine(xDoc);

В этом коде добавлены инструкции обработки как к документу, так и к элементу Employee. Прежде чем отобразить результат, обратите внимание на то, насколько хорошо здесь проходит функциональное конструирование. Очень легко создать дерево XML с двумя инструкциями обработки. И, наконец, вот результат:

Создание инструкции обработки на уровне документа и элемента

Создание потоковых элементов с помощью XStreamingElement

Ранее объяснялось, что многие из стандартных операций запросов в действительности откладывают свою работу до момента, пока не начнется перечисление возвращаемых ими данных. Если вызывается операция, которая фактически откладывает свое выполнение, и нужно спроектировать вывод запроса в виде XML, возникает дилемма. С одной стороны, есть желание воспользоваться преимуществом отложенной природы операции и выполнить работу, только когда в ней возникнет необходимость. Но с другой стороны, вызов API-интерфейса LINQ to XML заставит запрос выполняться немедленно.

Обратите внимание в примере ниже, что даже несмотря на то, что четвертый элемент массива names был изменен при выводе значения объекта XElement, дерево XML содержит его исходное значение. Это связано с тем, что элемент xNames был полностью создан перед изменением элемента массива names:

string[] names = { "John", "Paul", "George", "Pete" };
XElement xNames = new XElement("Beatles",
                from n in names
                select new XElement("Name",n));

names[3] = "Ringo";
Console.WriteLine(xNames);

Перед обсуждением результатов этого кода обратите внимание, насколько он замечателен. В коде создается элемент по имени Beatles с содержимым, состоящим из последовательности объектов XElement, элементы которых названы Name. Этот код производит следующее дерево XML:

Немедленное выполнение конструирования дерева XML

Впечатляюще. Каждый объект XElement из последовательности становится дочерним элементом. Чем это хорошо? Как уже упоминалось, несмотря на изменение names [3] в "Ringo" перед выводом XML, последний элемент по-прежнему содержит имя Pete — исходное значение. Причина в том, что последовательность names должна быть полностью перечислена, чтобы сконструировать объект XElement, а это требует немедленного выполнения запроса.

Если действительно нужно, чтобы конструирование дерева XML было отложено, необходим какой-то другой способ, и именно для этого предназначены потоковые (streaming) элементы. В LINQ to XML потоковые элементы реализованы классом XStreamingElement. Ниже показан тот же пример, но на этот раз вместо XElement используются объекты XStreamingElement:

string[] names = { "John", "Paul", "George", "Pete" };
XStreamingElement xNames = new XStreamingElement("Beatles",
                from n in names
                select new XElement("Name",n));

names[3] = "Ringo";
Console.WriteLine(xNames);

Если это работает, как было описано выше, то теперь значение Name последнего узла должно быть Ringo, а не Pete. И вот доказательство:

Демонстрация отложенного выполнения конструирования дерева XML за счет использования класса XStreamingElement
Пройди тесты
Лучший чат для C# программистов