Генерируем OfficeOpenXML-документы за 5 минут

    Часто надо бывает из приложения на ASP.NET сгенерировать отчёт на сервере в OpenXML-формате.

    Есть несколько привычных способов сделать это:
    1. «Нашёл, слинковал, заюзал» – идём в Гугл, ищем библиотеку для генерации docx или xlsx, подключаем, разбираемся, генерируем. Это привычно, но долго.
    2. «Фу» – использовать COM. Это не рекомендуется, требует установленного Microsoft Office на сервере, не очень thread-safe, с x64 не дружит и вообще старомодно.
    3. «Ъ» – разобраться с форматом, собрать из XML и зазипать. Брутально.
    4. «Microsoft way» – об этом способе рассказывается под катом.


    Небольшое введение


    OfficeOpenXML – это то, в чём вы по умолчанию сохраняете документы, работая в Word и Excel: docx и xlsx. Файл представляет собой zip-архив. Его можно переименовать в zip, открыть архиватором и рассмотреть, что внутри:
    OfficeOpenXML Folder View
    Отчёты в OOXML хорошо воспринимаются и редактируются привычными средствами. Я бы не рекомендовал в серьёзных приложениях ограничиваться именно этим форматом, но советую поддерживать его.

    Подготовка


    Нам понадобятся:
    Качаем OpenXMLSDKTool с сайта Microsoft и устанавливаем его:

    Setup

    Поехали


    Запускаем Open XML SDK 2.0 Productivity Tool:
    Productivity Tool
    Эта тулза очень простая и умеет делать две маленькие, но важные операции:
    • Сгенерировать код по документу
    • Сравнивать документы на уровне XML
    Но обо всём по порядку.

    Генерация кода


    Загружаем в программулину наш документ и кликаем «Reflect Code»:
    Reflect Code

    Слева мы видим структуру документа – те же файлы, что присутствуют в архиве, и представление их содержимого.
    Ноды в дереве можно выделять: справа видно содержимое ноды в виде XML и код, который может сгенерировать именно этот кусочек. На моём примере виден один абзац из тела документа. Оно как раз живёт в word/document.xml.
    Если выделить корень дерева (сам документ) – получим код для всего документа.

    Теперь давайте поиспользуем этот код
    1. Делаем проект в Visual Studio. Пусть это будет простое консольное C#-приложение
    2. Добавляем референс на сборку DocumentFormat.OpenXml:
      Add Reference
      У меня она в GAC. Если вы не хотите её туда класть, можно добавить ссылку на сам файл. Отдельно скачать его можно там же, где был OpenXMLSDKTool, но по ссылке OpenXMLSDKv2.msi
    3. Добавляем референс на WindowsBase
    4. Добавляем файл «GeneratedClass.cs»
    5. Копируем туда код из тулзы, из окошка ReflectedCode
    6. Закрываем файл, сохранив его, переходим в Program.cs
    7. Пишем метод Main:
      new GeneratedCode.GeneratedClass().CreatePackage(@"D:\Temp\Output.docx");
    8. Запускаем
    Всё. Код для генерации документа готов. Документ будет выглядеть точно так же, как он выглядел перед тем, как вы сохранили его в Word. Быстро, не правда ли?

    Что внутри?

    Что же внутри сгенерированного класса?
    Во-первых, там один единственный открытый метод:
    public void CreatePackage(string filePath) {
      using (WordprocessingDocument package = WordprocessingDocument.Create(filePath, WordprocessingDocumentType.Document)) {
        CreateParts(package);
      }
    }

    Вот тут вставляется текст, который будет в документе:
    private void GenerateMainDocumentPart1Content(MainDocumentPart mainDocumentPart1) {
      Run run2 = new Run() { RsidRunProperties = "00184031" };
      Text text2 = new Text();
      text2.Text = "Исчисление предикатов, по определению, философски выводит структурализм, изменяя привычную реальность."; // о.О какую траву курил Яндекс?
    }

    Как видно из названий private-методов в коде, OpenXml-документ состоит из частей (part). Для генерации каждой части сделан отдельный метод.
    Наиболее любознательные, конечно же, ехидно улыбнувшись, вставили в документ картинку.
    Картинки хранятся прямо в этом файле, в виде base64, вот тут:
    #region Binary Data
    //...
    #endregion

    Завязываем бантики

    Рефакторинг картинок и замена статического контента на динамический оставим читателю в качестве упражнения.
    А вот метод, который генерирует не файл, а массив байтов – для отдачи клиенту из asp.net без временных файлов:
    public byte[] CreatePackageAsBytes() {
      using (var mstm = new MemoryStream()) {
        using (WordprocessingDocument package = WordprocessingDocument.Create(mstm, WordprocessingDocumentType.Document)) {
          CreateParts(package);
        }
        mstm.Flush();
        mstm.Close();
        return mstm.ToArray();
      }
    }

    Всё, код для генерации отчёта в формате docx готов.
    Осталось заменить контент на динамический. Мы же не делали всё это ради того, чтобы всё время отдавать одно и то же, ведь правда? И добавить на страничку ссылку «Скачать в формате Word».

    Сравнение документов


    Итак, мы сгенерировали код по документу. Добавили туда много данных, зарефакторили его, внедрили в production. И вот нам надо поменять шрифт и текст в отчёте. Как же это сделать? Кода много, искать в нём долго.
    Оказывается, всё очень просто, нам поможет фича сравнения документов:
    1. Положим рядом старый и новый документы
    2. Открываем Open XML Productivity Tool, выбираем «Compare files...»:
      Compare Dialog
    3. Открываем файлы и жмём OK. Перед нами результат сравнения:
      Result

      На строчки с именами файлов можно тыкнуть и увидеть, в чём именно отличия:
      Comparison Details

      В MoreOprions выбирается, что игнорировать при сравнении.
      View Part Code показывает код той части, XML которой вы видите.
      Уж сопоставить XML и код труда не составит.

    Кстати, эту фичу ещё очень удобно использовать, если вы только знакомитесь с форматом OpenXML: добавляете что-то в документ и смотрите, что изменилось. Поможет тем, кто выбрал способ «Ъ», о котором говорилось в начале статьи.

    Факты

    • С Xlsx катит. Точно так же, как с docx
    • Если внутри Docx график или диаграмма – всё будет хорошо
    • Это всего лишь strongly-typed обёртка над библиотекой System.IO.Packaging
    • На сервере не нужно ничего, кроме этой библиотеки
    • Никаких проблем с x64
    • Производительность на высоте

    Выводы


    Я считаю, что использование DocumentFormat.OpenXml для генерации отчётов в web-приложениях – правильный выбор. Полезная тулза из SDK позволит вам не тратить время зря.

    Что почитать


    Про OpenXML SDK: msdn.microsoft.com/en-us/library/bb448854(office.14).aspx
    Про OpenXML (если кто с ним не знаком): en.wikipedia.org/wiki/Office_Open_XML

    Удачи! Спасибо за внимание.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 30
    • +1
      О, это интересно.
      Для полноты картины, ни подскажете, как сгенерированный док можно распечатать без «MS Office»?
    • 0
      Спасибо, как один из вариантов пригодится. Вроде, телодвижений совсем немного.
      • +6
        Использовал эту библиотеку для генерации xlsx отчетов для определенных входных данных, которые потом должны анализироваться экспертами. Так получилось, что отчеты могли содержать сотни тысяч строк(данные записывались в таблицы с возможностью фильтрации по столбцам итд).

        Вот тут проблемы и начинались, сейчас уже не помню, но в процессе создания документа память расходовалась катастрофически, помоему возникала ситуация когда процесс мог занимать до 4гб оперативной памяти(могу наврать, но с OutOfMemoryException ситуация была) и это для генерации файла который в итоге занимал 800кб. Причем бороться с этим сложно, так как документ при открытии загружается полностью и попытка писать порциями не дает ничего.

        Сохранение такого документа тоже затратно по времени(например время на процессинг данных занимал 4 секунды, экспорт в xlsx — 16 секунд). В общем для подобных случаев больше подходит генерация xml с применением xslt-преобразования для openxml, сурово, но быстро :) Или сторонние библиотеки для работы с openxml, они есть, но платные. Также не понравилась как реализованы фичи типа добавления стилей в документ, настолько все через задницу, что пользоваться тяжело, в общем лучше бы они нормальное api писали для генерации документов, а не то что сейчас.
        • 0
          На таких больших данных не тестировал, спасибо что предупредили — думаю будет полезно. А на обычных отчётах, которые юзверей экспортируют, работает приемлемо.
          Кстати, так плохо себя показала 1я или 2я версия библиотеки?
          • 0
            2ая версия, с первой не довелось сталкиваться.
        • –1
          Уже год как по рунету ходит способ генерации файлов Microsoft Word 2007 вообще без какого-либо кода, т.е. на коленке — методом copy-paste.

          Кстати, автор — хабрапользователь. Только он забанен на сайте в данный момент.
          • +2
            Это третий вариант + изобретание велосипеда из BCL (встроенный Sysytem.IO.Packaging заменили на ручную компресию через либу зипования. Короче двойной велосипед. Так сказать чертыре колеса.
          • –1
            >>>Его можно переименовать в zip, открыть архиватором и рассмотреть, что внутри…
            Hint: А если у вас есть Total Commander (а он есть у многих), то достаточно выполнить над файлом Ctrl+PageDown, и вы заходите в .docx как в обычный каталог. Ну разве это не прелесть?
            • 0
              Прелесть, постоянно пользуюсь. Ещё здорово, что он умеет в msi-ки так заходить.
              • 0
                и в Far так же.
                • 0
                  ctrl+стрелка вверх откроет в новой вкладке.
                  А еще можно запихнуть внутрь документа xml и прибиндить его к полям внутри документа (биндинг двухсторонний, на поля можно установить запрет редактирования/удаления).
                • 0
                  Если инсталл создаем своего приложения, то нужно только эту библиотеку включать?
                  Не проверяли, насколько больше размер инсталяшки становится?
                  Понятно, что в любом случае это меньше (и бесплатней), чем офис :) Для виртуальной машины вообще замечательно.

                  Сравнение файлов — это сервис для приложений с многоверсионностью документов? Не совсем понял для чего юзать сравнение файлов.
                  • 0
                    Да, включать в инсталл надо только её, её размер — около 5 мб, в зазипанном виде — 1.3 мб. Размер инсталляшки увеличится на что-то среднее между.
                    Сравнение файлов — это сравнение двух доков на уровне xml. Я юзаю для того, чтобы посмотреть, что именно вставит word в xml, например, чтобы сделать абзац цветом (иногда это неочевидно). Оттуда копирую кусочек кода и вставляю в свой код, который генерит отчёт.
                    Например, надо добавить колонтитул. Неохота лезть в доки по OpenXml или гуглить. Добавил в ворде, сравнил со старой версией — и вот ответ. В качестве плюшки есть даже и код, можно скопипастить.
                  • –7
                    > Его можно переименовать в zip, открыть архиватором и рассмотреть, что внутри…

                    21-ый век… Виндовс и Ко определяют тип содержимого файла по расширению.

                    Есть очень неприятная задачка. Нжно генерировать docx на сервере на LAMP. Что можете посоветовать? Только брутальный вариант?
                    • 0
                      Можно проверить работает ли эта либа под Моно. Вероятность что работает высока, так как можно потдерживает ту часть которая свзяана ОПФ. Если попробуете, раскажите.
                      • +1
                        phpexcel, phpdocx, но библиотеки мострозные. если есть время — я бы посоветовал рахобраться с форматом на уровне XML и написать свои классы
                      • +1
                        Run run2 = new Run() { RsidRunProperties = "00184031" };

                        не люблю магические числа
                        • +1
                          Я так и не понял зачем необязательные RSid ы и rsidPr читать/писать.
                          Как пишет
                          The rsid element is used to allow applications to more effectively merge two documents that have forked. ориг

                          Они используются для более эффективного слияния изменений одного документа.
                        • 0
                          Оффтоп:
                          Скажите, откуда у вас такое удобное меню добавления сборок в проект взялось? Это как часть SP1?
                          • НЛО прилетело и опубликовало эту надпись здесь
                            • 0
                              Заметил неприятную особенность с этим аддоном.

                              Допустим, мы имеем 2 солюшена в двух отдельных папках, у каждого свой Output:
                              Shared, Output: Shared/bin
                              Main, Output: Main/bin

                              Когда добавляю референс из проекта Main на один из проектов Shared, почему-то Location референса вместо Shared/bin/… прописывается Main/bin/…

                              Это приводит к проблемам при автоматизации сборок (TFS, TeamCity), пришлось отключить.
                              • 0
                                Очень странная штука. Напишите им, думаю, пофиксят — всем будет лучше
                                • НЛО прилетело и опубликовало эту надпись здесь
                            • 0
                              Для генерации документу нужен установленный Office?
                              Есть варианты без него?
                              • 0
                                Нет, офис не нужен. Нужна только сборка DocumentFormat.OpenXml.
                              • 0
                                Кто про ODF напишет подобное? :)
                                • 0
                                  А при создании десктопного ли Silverlight приложения проще использовать Custom XML Parts
                                  • 0
                                    Эх, если бы кто написал, как за приемлемое время можно из docx получить html — ценыб ему бы не было. Особенно в свете приближающегося МЭДО.

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