Пакет Open XML SDK: основные сведения
Привет! Меня зовут Али. Я являюсь разработчиком в команде разработчиков Word. Я также работал над функциональностью пакета Open XML SDK и в этой публикации хочу подробнее рассказать о пакете SDK и лежащей в его основе концепции.
В своих записях Зияд Раджаби (Zeyad Rajabi) и Эрик Уайт (Eric White) продемонстрировали множество отличных решений (например, "Использование преимущества связанных элементов управления содержимым" и "Принятие исправлений и удаление примечаний из документа SharePoint"), которые можно построить с помощью пакета Open XML SDK для создания, чтения или изменения документов, электронных таблиц и презентаций. В каждой записи присутствовал сценарий и был продемонстрирован код для его реализации. Здесь я хочу более подробно обсудить содержимое пакета SDK и то, чем обусловлены некоторые из представленных в нем подходов. У меня нет сценария, есть лишь некоторые заметки, дающие представление о том, из чего состоит пакет SDK и почему.
Я работаю над Word, поэтому все мои примеры будут касаться компонентов WordProcessingML, хотя все представленное применимо и к другим форматам.
Это всего лишь XML!
Помимо пакетов и разделов пакет SDK включает иерархию типов, таких как Paragraph. Для успешного использования SDK важно знать, что эти классы всего лишь представляют элементы XML и действуют так же, но с несколько большей интеграцией с типами.
В корне иерархии располагается (абстрактный) класс OpenXMLElement. Класс OpenXMLElement программно представляет любой старый элемент, фигурирующий в файле Open XML. Имя его довольно ясно представляет элемент XML. Предоставленные методы и свойства также соответствуют ожиданиям в отношении элементов XML: .FirstChild, .InnerText, .NextSibling(), Append(OpenXMLElement) и т.д.
Класс OpenXMLElement сам по себе предоставляет некоторые функции (как все хорошие базовые классы), которые мы рассмотрим ниже, но производные от него классы представляют больший интерес. Для каждого элемента, указанного в спецификации Open XML, существует соответствующий класс в SDK, производный (опосредовано) от класса OpenXMLElement. Во-первых, существует еще два абстрактных класса, предназначенных для того, чтобы отличать элементы, которые могут иметь другие элементы в качестве дочерних и те, которые не могут, — это классы OpenXMLCompositeElement и OpenXMLLeafElement.
Класс Paragraph, например, является производным от класса OpenXMLCompositeElement. "Ну и что?" — спросите вы. Хорошо, начнем с простого. Если вам приходилось писать код, управляющий объектами XmlElement, вы знаете, как трудно постоянно сверять имя и пространство имен, чтобы определить, с каким элементом вы в данный момент работаете. Благодаря иерархии типов, представляющей элементы, вы всегда получаете наиболее производный тип для элемента из методов Open XML SDK, возвращающих объекты OpenXMLElement. Например, свойство Body.FirstChild возвращает экземпляр класса Paragraph, если первый дочерний элемент элемента w:body в считываемом документе действительно является элементом w:p. В противном случае возвращается какой-либо другой тип. Так, вместо проверки имени и пространства имен элемента можно проверить его тип, например:
Paragraph p = child as Paragraph;
if (p != null)
{
// Do something ...
}
Я хочу лишний раз подтвердить, что тут нет никакой магии. Пакет SDK действительно проверил имя и пространство имен для выбора класса Paragraph подобно тому, как сделал бы ваш код. Дополнительное преимущество заключается в том, что вам не надо каждый раз писать эти проверки самостоятельно. Конечно, наличие типа имеет и другие преимущества. Для начала можно написать функции, принимающие аргумент типа Paragraph, а компилятор будет следить за выполнением этого контракта вместо вас.
Более того, когда элементы различаются по типу, можно использовать универсальные типы для обеспечения большей чистоты кода. Помните, что я упоминал о том, что OpenXMLElement сам по себе обладает некоторыми полезными функциями? Взгляните на метод GetFirstChild<T>, где T является производным от OpenXMLElement. Как можно представить, этот метод возвращает первый дочерний элемент, принадлежащий определенному типу. Снова все, что делает SDK, это проверяет имя и пространство имен вместо вас, но полученная очистка кода существенна. Descendants<T>(), RemoveAllChildren<T>(), ReplaceChild<T>(), Ancestors<T>() и прочие методы действуют подобным образом.
Еще одним преимуществом наличия этих типов является возможность использования ключевого слова "new". Хотите создать элемент w:b (полужирный)? Просто сделайте это:
Bold b = new Bold();
Помните, что это просто особый способ работы с XML. Что я сделал? Я создал элемент XML в пространстве имен WordProcessingML (конечно, в зависимости от того, в какой оператор using разрешается тип Bold) с именем "b". Этот элемент еще не принадлежит какому-либо документу, его необходимо добавить (с помощью метода InsertAfter, AppendChild и т.д.).
Наличие спецификации Open XML позволило пакету SDK представлять отдельные элементы как собственные типы и добавлять функции, упрощающие обработку элементов благодаря их типам.
Свойства первого класса
До этого момента мы использовали схему только как список имен и пространств имен элементов, хотя этот список и был очень большим. Схема сообщает нам гораздо больше, в частности, какие элементы и атрибуты допустимы в качестве дочерних для того или иного элемента. Например, элемент w:sdt (тег структуры документа уровня последовательности знаков) в WordProcessingML может иметь максимум один дочерний элемент типа w:sdtPr (свойства SDT). Учитывая изложенное выше, мы можем использовать универсальный метод GetFirstChild<T>() для поиска этого элемента свойств следующим образом:
SdtProperties sdtPr = sdtRun.GetFirstChild<SdtProperties>();
if (sdtPr != null)
{
// Do something ...
}
Следует заметить, что нам не надо приводить возвращаемое значение GetFirstChild. Это лучше, чем циклический перебор всех дочерних элементов с проверкой имен и пространств имен с целью поиска элемента sdtPr, но по-прежнему не самый простой способ, поскольку он требует повтора действий на каждом уровне. Класс SdtRun в SDK имеет свойство SdtProperties, представляющее этот дочерний элемент. Таким образом, фрагмент кода будет иметь следующий вид:
SdtProperties sdtPr = sdtRun.SdtProperties;
if (sdtPr != null)
{
// Do something ...
}
Это свойство доступно для чтения и записи. Теперь возьмем объект RunProperties.
RunProperties rPr = new RunProperties();
rPr.Italic = new Italic();
rPr.Bold = new Bold();
rPr.NoProof = new NoProof();
Что произошло? Мы создали элемент w:rPr и присвоили его свойство Bold новому экземпляру класса Bold. При этом был создан элемент w:b, который при присвоении был присоединен в качестве дочернего к нашему элементу rPr. То же происходит и со свойствами Italic и NoProof. Снова никакой магии тут нет, это всего лишь сокращенные варианты операций, выполняемых как операции с исходным кодом XML, позволяющие значительно сократить и упростить код.
В этих четырех строках скрыта еще более ценная функция. Ниже представлен аналогичный код, но без использования свойств первого класса. Что тут не так?
RunProperties rPr = new RunProperties();
rPr.AppendChild(new Italic());
rPr.AppendChild(new Bold());
rPr.AppendChild(new NoProof());
Этот фрагмент кода создает документ с недопустимой схемой. В схеме указаны дочерние элементы элемента rPr как последовательность, поэтому порядок имеет значение. Согласно схеме, чтобы файл был допустимым, Bold (w:b) должен следовать перед Italics (w:i). Фрагмент кода, в котором использовано присвоение свойств, выполняет эту задачу правильно (поскольку фоновый код присвоений "знает" о порядке, которому должен подчиняться второй код).
Теперь подведем итог того, что мы увидели в примерах. Если схема разрешает использование только одного экземпляра дочернего элемента, в класс, представляющий родительский элемент, добавляется свойство этого типа и с тем же именем. Значение свойства может быть доступно для чтения или записи, а присвоение свойству добавляет (или переопределяет) дочерний элемент по отношению к родительскому.
А как же атрибуты? Атрибуты по определению отвечают приведенным выше критериям (каждый атрибут в каждом элементе может фигурировать не более одного раза). Так, атрибуты, объявленные в схеме, всегда получают свойства первого класса. Например, к приведенным выше свойствам выполнения мы можем добавить Underline.
RunProperties rPr = new RunProperties();
rPr.Italic = new Italic();
rPr.Bold = new Bold();
rPr.Underline = new Underline();
rPr.Underline.Val = UnderlineValues.DotDotDash;
Последняя строка представляет наибольший интерес. Во-первых, обратите внимание на свойство Val в элементе Underline. Оно представляет атрибут w:val этого элемента. Как обычно, выполняя присвоение этому свойству, мы создаем экземпляр атрибута и помещаем его в элемент. Значения атрибута, конечно, строковые, но, если схема указывает, что они являются членами перечисления, SDK также содержит соответствующее перечисление CLR. Имя перечисления создается таким, чтобы оно было узнаваемо: например, TypeValues. Здесь можно долго говорить о специфике и "удобстве" синтаксиса присвоения, предполагающего неявные конструкторы и прочее, но я оставлю это для самостоятельного изучения читателями. Укажу лишь на то, что атрибут Val здесь жестко типизирован. Следующий оператор скомпилирован не будет:
rPr.Underline.Val = BooleanValues.False;
Это всего лишь XML! Правда!
Ладно, вернемся к началу. Я надеюсь, что мне удалось продемонстрировать, как SDK добавляет значение, используя компактные и простые способы, позволяющие создавать более понятный и чистый код для создания, чтения файлов Open XML и работы с ними. В заключение упомяну еще некоторые моменты.
- Не позволяйте свойствам вводить вас в заблуждение, считая, что объекты всегда присутствуют. В действительности вы оперируете элементами и атрибутами XML и порой они просто отсутствуют. rPr.Underline может вернуть значение NULL, например, если w:rPr не имеет дочернего элемента Underline. Даже если значение не NULL, rPr.Underline.Val может вернуть значение NULL, если элемент присутствует, но отсутствует атрибут Val.
- Создавать элементы с помощью ключевого слова "new" очень просто и интересно, но даже такой способ может оказаться трудоемким из-за большого размера иерархии элементов, в частности, когда создаваемые элементы подобны. Например, представьте, что вашему приложению требуется добавить таблицу в документ. Кроме того, первые три строки таблицы всегда одинаковы (скажем, заголовок). Вместо создания объекта Table, трех объектов Row и множества объектов Cell для каждого из объектов Row с последующим добавлением ячеек в строки, а строк — в таблицу, можно просто воспользоваться конструкцией, предоставленной классом Table, которая использует в качестве входящего значения строку XML. Конечно, предоставляемый XML должен иметь надлежащий формат (т.е. необходимо будет закрыть теги w:tbl и т.д.), но затем можно будет добавить в полученный объект Table дополнительные строки.
- LINQ. В большинстве примеров, приведенных Зиядом и Эриком, используется исключительно LINQ. Хотя Open XML SDK разрабатывался для работы с LINQ, явного требования использовать LINQ здесь нет. Как правило, при создании элементов XML (таких, как приведенные выше примеры с элементом rPr) простой императивный код оказывается более удобочитаемым, чем аналоги LINQ. Точно так же запросы LINQ (лишний раз повторюсь) оказываются короче, когда необходимо задать такой простой вопрос, как "содержит ли этот документ какие-либо теги SDT с данным значением псевдонима?".
-Али
Это локализованная запись блога. Исходную статью можно найти по адресу https://blogs.msdn.com/brian_jones/archive/2009/01/12/open-xml-sdk-the-basics.aspx.