Программная генерация документов-форм в Word

8409840image001Продолжаем, начатую ранее тему работы с формами в Word. В предыдущих статьях мы смотрели на формы только с точки зрения “продвинутого пользователя”, т.е. мы создавали документы, удобные для ручного заполнения. Сегодня же я хочу предложить расширить эту задачу и попробовать использовать механизм Content controls для генерации документов.

Прежде, чем мы приступим к нашей непосредственной задаче, хочу сказать пару слов по поводу того, как хранятся в документах Word данные для сontent controls (то как они привязываются к содержимому документа я сознательно пока опущу, но надеюсь вернуться к этому как-нибудь в следующих статьях).

Как мы уже видели ранее (Open Packaging Conventions #2. Собираем MS Word документ руками) вся основная часть документа хранится в Main Document Part. Все остальные компоненты: картинки, данные данные библиографии, … и конечно же данные для content controls привязываются к нему.

Приблизительно как показано на рисунке:

image

Каждый компонент item1.xml, item2.xml и т.д. (в файлах, сформированных Word они называются так и лежат обычно в папке /customXml) хранит данные по каждому подключенному в документ источнику данных. Мы даже это наблюдали ранее, когда только осваивали создание форм в Word в статье Таблицы в формах Word 2013!

Закономерный вопрос – а что такое itemProps1.xml и аналогичные компоненты? В этих компонентах хранятся описания источников данных. Скорее всего, по задумке разработчиков помимо встроенных в документ xml-ек, предполагалось использовать и другие, но пока реализован только этот способ.

Чем полезны нам itemPropsX.xml? Тем, что в них перечислены xml-схемы (их targetNamespace), которые используются в родительском itemX.xml. Это значит, что если мы подключили в документ не одну custom xml, то чтобы найти нужную, нам нужно пробежаться по itemPropsX.xml компонентам и найти нужную схему, а значит и нужный itemX.xml.

Теперь еще один момент. Мы не будем вручную анализировать связи между компонентами и искать нужные, используя только базовый Packaging API! Вместо этого мы воспользуемся Open XML SDK (его сборки доступны через NuGet). Конечно, ранее мы не словом не говорили про этот API, но для нашей задачи от него требуется минимум и весь код будет достаточно прозрачен.

Ну что ж, основное введение сделано, можно приступать к примеру.

По сложившейся традиции возьмем все тот же “Отчет о совещании”, который мы рисовали в статье Таблицы в формах Word 2013. Напомню, что вот так выглядел шаблон документа:

image

А вот так, XML к которому привязывались поля документа

<?xml version="1.0" encoding="utf-8"?>
<meetingNotes xmlns="urn:MeetingNotes" subject="" date="" secretary="">
    <participants>
        <participant name=""/>
    </participants>
    <decisions>
        <decision problem="" solution="" responsible="" controlDate=""/>
    </decisions>
</meetingNotes>

Шаг 1. Создание модели данных

Собственно наша задача не просто сгенерировать документ, а создать (хотя бы в черновом варианте) удобный инструмент для использования как разработчиком, так и пользователем.

Поэтому модель мы объявим в виде структуры С#-классов:

[XmlRoot("meetingNotes", Namespace = "urn:MeetingNotes")]
public class MeetingNotes
{
    public MeetingNotes()
    {
        Participants = new List<Participant>();
        Decisions = new List<Decision>();
    }
 
    [XmlAttribute("subject")]
    public string Subject { get; set; }
 
    [XmlAttribute("date")]
    public DateTime Date { get; set; }
 
    [XmlAttribute("secretary")]
    public string Secretary { get; set; }
 
    [XmlArray("participants")]
    public List<Participant> Participants { get; set; }
 
    [XmlArray("decisions")]
    public List<Decision> Decisions { get; set; }
}
 
[XmlType("decision")]
public class Decision
{
    [XmlAttribute("problem")]
    public string Problem { get; set; }
 
    [XmlAttribute("solution")]
    public string Solution { get; set; }
 
    [XmlAttribute("responsible")]
    public string Responsible { get; set; }
 
    [XmlAttribute("controlDate")]
    public DateTime ControlDate { get; set; }
}
 
[XmlType("participant")]
public class Participant
{
    [XmlAttribute("name")]
     public string Name { get; set; }
}

По большому счету ничего особенного, разве что добавлены атрибуты для управления XML-сериализацией (т.к. имена в модели и требуемой XML немного различаются).

Шаг 2. Сериализация приведенной выше модели в XML

Задача, в принципе, тривиальная. Что называется “берем наш любимый XmlSerializer и вперед”, если бы не одно но

К сожалению, в текущей версии Office, по всей видимости, присутствует баг, который заключается в следующем: если в custom xml перед объявлением основного namespace (того, из которого Word должен брать элементы для отображения), объявить еще какой-нибудь, то повторяющиеся Content controls начинают отображаться не верно (показывается только столько элементов, сколько было в самом шаблоне – т.е. repeating section не работает).

Т.е. вот такой xml работает:

<?xml version="1.0" encoding="utf-8"?>
<test xmlns="urn:Test" attr1="1" attr2="2">
    <repeatedTag attr="1" />
    <repeatedTag attr="2" />
    <repeatedTag attr="3" />
</test>

и вот такой тоже:

<?xml version="1.0" encoding="utf-8"?>
<test xmlns="urn:Test" attr1="1" attr2="2" xmlns:t="urn:TTT">
    <repeatedTag attr="1" />
    <repeatedTag attr="2" />
    <repeatedTag attr="3" />
</test>

а вот такой, уже нет:

<?xml version="1.0" encoding="utf-8"?>
<test xmlns:t="urn:TTT" xmlns="urn:Test" attr1="1" attr2="2">
    <repeatedTag attr="1" />
    <repeatedTag attr="2" />
    <repeatedTag attr="3" />
</test>

я пробовал отправить баг в поддержку Microsoft на Connect, но у меня почему-то закрыт доступ для отправки багов по Office. А обсуждение на форуме MSDN тоже не помогло.

В общем, нужный обходной маневр. Если бы мы формировали XML руками, проблем бы не возникло – мы сделали бы все сами. Однако в данном случае очень хочется использовать стандартный XmlSerializer, который по-умолчанию добавляет несколько своих namespace в выходной XML, даже если эти namespace не используются.

Мы сделаем полное подавление вывода собственных namespace в XmlSerializer. Правда, этот подход сработает, только если они ему и правда будут не нужны (в противном случае они все равно будут добавлены и как раз ДО нашего Печальная рожица).

Собственно, весь код (при условии, что переменная meetingNotes содержит ранее заполненный объект типа MeetingNotes):

var serializer = new XmlSerializer(typeof(MeetingNotes));
var serializedDataStream = new MemoryStream();

var namespaces = new XmlSerializerNamespaces();
namespaces.Add(“”, “”);

serializer.Serialize(serializedDataStream, meetingNotes, namespaces);
serializedDataStream.Seek(0, SeekOrigin.Begin);


Шаг 3. Заносим полученную XML в Word-документ.

Тут мы поступаем следующим образом:

  • копируем шаблон и открываем копию
  • находим в ней нужный custom xml (ищем по namespace “urn:MeetingNotes”)
  • замещаем содержимое компонента, на нашу XML
File.Copy(templateName, resultDocumentName, true);
 
using (var document = WordprocessingDocument.Open(resultDocumentName, true))
{
    var xmlpart = document.MainDocumentPart.CustomXmlParts
        .Single(xmlPart =>
            xmlPart.CustomXmlPropertiesPart.DataStoreItem.SchemaReferences.OfType<SchemaReference>()
            .Any(sr => sr.Uri.Value == "urn:MeetingNotes"));
 
    xmlpart.FeedData(serializedDataStream);
}

Все! У нас готовый документ-отчет по совещанию. Причем, мы реализовали весьма удобный для повторного использования подход (источником данных для данного отчета может служить все, что угодно).

Как поиграться?

Пример, который приведен в статье, можно как и ранее найти на GitHub.

Если же хочется попробовать пример в деле (и не хочется разбираться в коде), можно посмотреть Web-реализацию примера генерации отчета, а исходный код этого примера найдется там же на Codeplex, но в соседней ветке.

image

This entry was posted in MS Office and tagged , , . Bookmark the permalink.

Leave a comment