С выходом .NET5 дальнейшее развитие некоторых проектов оказалось под вопросом из-за сложности портирования. Если от небольших устаревших библиотек можно отказаться или найти им замену, то от зависимости Microsoft.Office.Interop.Word.dll очень сложно отказаться. Microsoft не планирует добавлять совместимость с .NET Core/5+, поэтому в этой статье мы рассмотрим, как создавать документы Word с помощью Open XML SDK.

Введение

Office Open XML, также известный как OpenXML или OOXML, представляет собой формат на основе XML для офисных документов, включая текстовые документы, электронные таблицы, презентации, а также диаграммы, фигуры и другой графический материал. В июне 2014 года Microsoft выпустила исходный код Open XML SDK на GitHub для работы с таким форматом.

У этой библиотеки есть серьёзные преимущества:

  • совместима с .NET 5+,

  • не требует установки Microsoft Office,

  • высокая скорость работы,

  • открытый исходный код.

Без минусов тоже не обошлось:

  • сложный API,

  • скудная документация.

Эти минусы определённо дополняют друг друга. Собственно, это и стало причиной создания этого материала.

А вот открытый исходный код является большим плюсом. Если бы код COM-библиотек был открыт, сообщество разработчиков помогло бы с портированием на .NET Core/5+. Кроме привлечения сторонних разработчиков, публичный код даёт каждому возможность находить и исправлять ошибки и уязвимости или хотя бы сообщать о них. Качество публичных библиотек очень важно для всех проектов, которые могут их использовать. Например, мы проводили небольшой аудит кода Open XML SDK при первом знакомстве с этой библиотекой.

Боль разработчиков Office

Для продуктов Office было разработано очень много софта сторонними разработчиками. Это плагины для Word, Excel, Outlook. Многие компании наделали себе удобных плагинов и генераторов отчётов в формате Word. А 3 июля 2021 года произошло страшное – все тикеты про поддержку .NET 5+ в VSTO / COM, разбросанные по разным ресурсам, были в одночасье закрыты с комментарием представителей Microsoft подобного рода:

...The VSTO/COM Add-Ins platform is very important to Microsoft, and we plan to continue to support it in Office with .NET Framework 4.8 as the last major version...VSTO/COM Add-Ins cannot be created with .NET Core and .NET 5+. This is because .NET Core/.NET 5+ cannot work together with .NET Framework in the same process and may lead to add-in load failures. Microsoft will not be updating VSTO or the COM Add-in platform to use .NET Core or .NET 5+...

По их информации, поддержка .NET 5+ не предвидится. Вот одна из таких дискуссий, которая ещё долго не прекращалась после этого объявления: "Please port Visual Studio Tools For Office (VSTO) to .NET 5/7, to enable VSTO add-in development in C# in .Net 5/7".

Если у разработчиков плагинов совсем всё плохо – им предложили перейти на Office JavaScript API (совсем другой язык, API не позволяет делать и малую часть того, что было), то для создания документов из C# кода можно попробовать перейти на библиотеку Open XML SDK (nuget).

Основы

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

Word документ — это набор запакованных xml-документов. Все элементы структурированы под тегами.

Например, параграф внутри документа будет выглядеть примерно вот так:

<w:p w:rsidR="007D2247" w:rsidRDefault="009A4B44"
         xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:r>
    <w:t>тест</w:t>
  </w:r>
  <w:bookmarkStart w:name="_GoBack" w:id="0" />
  <w:bookmarkEnd w:id="0" />
</w:p>

Сборка Interop.Word немного абстрагируется от этой структуры и часто работает с некоторым участком – Range – документа. А Open XML SDK идёт по пути отражения внутренней структуры документа в самом коде. Параграфы <w:p>, участки текста <w:t> и всё остальное становятся объектами в самом коде. Если вы не создадите тело документа, параграф и других обязательных "родителей", то и добавлять текст будет некуда.

На скриншоте как раз изображена внутренняя структура основного файла для документа Word – document.xml. Этот файл содержит само наполнение документа.

Скриншот сделан в очень нужной для работы с Open XML утилите Open XML SDK 2.5 Productivity Tool. К моменту написания статьи эта утилита была удалена с сайта Microsoft, а в репозитории Open-XML-SDK добавлена ссылка на некий DocxToSource, который должен стать заменой устаревшего Productivity Tool. Однако эта замена всё ещё является прототипом, поэтому пока лучше постараться найти старый добрый Productivity Tool. Старая утилита позволяет просмотреть строение документа, познакомиться с автогенерированным кодом.

Также она позволяет сравнить два разных документа (и код для их создания, и их внутреннее строение).

Примеры

Для Interop.Word во всей статье примем такой псевдоним для удобства чтения:

using MicrosoftWord = Microsoft.Office.Interop.Word;

Также для упрощения будем называть Open XML SDK просто Open XML.

Создание документа

Interop.Word:

MicrosoftWord.Application wordApp = new MicrosoftWord.Application();
MicrosoftWord.Document wordDoc = wordApp.Documents.Add();
MicrosoftWord.Range docRange = wordDoc.Range();
.... // тут при необходимости работаем с документом
wordDoc.SaveAs2(pathToDocFile);
wordApp.Quit();

Тут всё достаточно просто, но всё равно есть свои подводные камни. При работе с Interop мы взаимодействуем не просто с некоторым объектом в памяти, а с COM-объектом. Поэтому возникает необходимость завершать все процессы после окончания работы программы. Эта проблема не раз поднималась на Stack Overflow (1, 2), и ей предложено множество разных решений.

Есть решение с участием Marshal Class, являющимся частью InteropServices.

finally
{
  if (Marshal.IsComObject(wordDoc))
    try
    {
      Marshal.FinalReleaseComObject(wordDoc);
    }
    catch { throw; }
 
  if (Marshal.IsComObject(wordApp))
    try
    {
      Marshal.FinalReleaseComObject(wordApp);
    }
    catch { throw; }
}

Однако в таком случае можно упустить какие-нибудь процессы.

Есть более надёжный вариант с обращением к GC:

GC.Collect();
GC.WaitForPendingFinalizers();

Эти методы надо вызвать после того, как вся работа с COM-объектами будет завершена.

Если не завершить процессы, то при активном дебаге можно устроить себе такую ситуацию:

Но даже если в коде присутствует закрытие процессов после окончания работы, при прерывании программы вручную или её падении процесс останется запущенным. Такого недостатка нет при создании и работе с документом через Open XML.

Open XML:

using (WordprocessingDocument doc = 
         WordprocessingDocument.Create(pathToDocFile,
                                       WordprocessingDocumentType.Document,
                                       true))
{
  MainDocumentPart mainPart = doc.AddMainDocumentPart();
  mainPart.Document = new Document();
  Body body = mainPart.Document.AppendChild(new Body());
  SectionProperties props = new SectionProperties();
  body.AppendChild(props);
}

Обратите внимание на добавление SectionProperties, они понадобятся нам позже.

Добавление параграфа

Interop.Word

public static void InsertWordText(MicrosoftWord.Document doc,
                                      string text)
{
  MicrosoftWord.Paragraph paragraph = doc.Paragraphs.Add(Missing.Value);
  paragraph.Range.Text = text;
  paragraph.Range.InsertParagraphAfter();
}

Текст также можно сделать жирным или курсивным через параметр Font:

paragraph.Range.Font.Bold = 1;
paragraph.Range.Font.Italic = 1;

Изменить размер шрифта можно через:

paragraph.Range.Font.Size = 14;

Выравнивание текста выполняется через ParagraphFormat.Alignment:

paragraph.Range.ParagraphFormat.Alignment = MicrosoftWord.WdParagraphAlignment
                                                        .wdAlignParagraphCenter;

Open XML:

public static void AddText(WordprocessingDocument doc, string text)
{
  MainDocumentPart mainPart = doc.MainDocumentPart;
  Body body = mainPart.Document.Body;
  Paragraph paragraph = body.AppendChild(new Paragraph());

  Run run = paragraph.AppendChild(new Run());
  run.AppendChild(new Text(text));
  run.PrependChild(new RunProperties());
}

В случае с Open XML жирным или курсивным текст можно сделать через:

run.RunProperties.AddChild(new Bold());
run.RunProperties.AddChild(new Italic());

Изменение размера шрифта в этом случае немного неинтуитивно, но согласуется с общей логикой работы с Open XML:

run.RunProperties.AddChild(new FontSize(){ Val = "14"});

Выравнивание текста:

paragraph.ParagraphProperties.AddChild(new Justification()
                                       {
                                         Val = JustificationValues.Center
                                       });

Важно перед этим не забыть добавить к параграфу свойства:

paragraph.AppendChild(new ParagraphProperties());

Вставка заголовка

Предположим, что нам нужно вписать в документ заголовок. В случае Interop.Word нужно всего лишь небольшое дополнение к вставке текста, чтобы получить заголовок:

Interop.Word:

public static void InsertWordHeading1(MicrosoftWord.Document doc,
                                      string headingText)
{
  MicrosoftWord.Paragraph paragraph = doc.Paragraphs.Add(Missing.Value);
  paragraph.Range.Text = headingText;
  paragraph.Range.set_Style("Heading 1");
  paragraph.Range.InsertParagraphAfter();
}

В этом случае сначала задаём Range для записи нового текста и присваиваем ему стиль Heading 1.

Open XML:

public static void InsertWordHeading1(WordprocessingDocument doc,
                                      string headingText)
{
  MainDocumentPart mainPart = doc.MainDocumentPart;
  Paragraph para = mainPart.Document.Body.AppendChild(new Paragraph());
  Run run = para.AppendChild(new Run());
  run.AppendChild(new Text(headingText));
  para.ParagraphProperties = new ParagraphProperties(
                               new ParagraphStyleId() { Val = "Heading1" });
}

Тут, казалось бы, всё очень похоже. Аналогично добавляем параграф и в случае с Open XML организуем нужную иерархию объектов.

Однако на самом деле в случае с Open XML коварным оказывается добавление стиля. Interop.Word работает с реальным полноценным документом, как если бы вы запустили Word и нажали создать. А вот Open XML работает только с тем, что было создано. И если вы добавляете текст документу, созданному через Open XML, а не через Interop.Word, то в нём будут отсутствовать, например, стили. Соответственно, никакого стиля Heading1 в таком документе не будет. Его нужно сначала добавить.

Удобнее всего добавлять нужный стиль при создании документа. Есть два варианта: перенести стили из готового Word-документа или добавить эти стили вручную.

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

Для второго варианта нам поможет Productivity Tool для Open XML, упоминавшийся ранее. Чтобы получить код, нужный для добавления желаемого стиля, создаём чистый документ Word, используем в нём нужный стиль и "скармливаем" этот документ утилите. Далее через использование кнопки Reflect Code на /word/styles.xml в структуре документа мы получим реализацию метода GeneratePartContent. В нём мы ищем реализацию нужного стиля и всё, что с ним связано, включая StyleParagraphProperties, StyleRunProperties и т.д.

Для стиля Heading 1 нужный нам автосгенерированный код будет выглядеть примерно так:

Style style2 = new Style() { Type = StyleValues.Paragraph,
                             StyleId = "Heading1" };
StyleName styleName2 = new StyleName(){ Val = "heading 1" };
....
style2.Append(styleRunProperties1);

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

var styles = new Styles();
styles.Append(style2);
wordDocument.MainDocumentPart.AddNewPart<StyleDefinitionsPart>();
wordDocument.MainDocumentPart.StyleDefinitionsPart.Styles = styles;

У себя мы решили использовать вариант с шаблонным документом, чтобы в будущем при появлении необходимости в каком-либо стиле нужно было лишь использовать его в шаблоне и работать с ним в коде вместо того, чтобы каждый раз рыться в ProductivityTool и копировать себе полотна кода с объявлением нужного стиля.

Смена ориентации страницы

Для нашего отчёта нам нужна была именно ландшафтная ориентация страницы.

Interop.Word:

MicrosoftWord.Document wordDoc = wordApp.Documents.Add();
MicrosoftWord.Range docRange = wordDoc.Range();
docRange.PageSetup.Orientation = MicrosoftWord.WdOrientation
                                              .wdOrientLandscape;

У документа получаем нужный Range (страниц или всего документа) и задаём ландшафтную ориентацию.

Open XML:

var sectionProperties = mainPart.Document
                                .Body
                                .GetFirstChild<SectionProperties>();
sectionProperties.AddChild(new PageSize()
{
  Width = (UInt32Value)15840U,
  Height = (UInt32Value)12240U,
  Orient = PageOrientationValues.Landscape
});

C Open XML в этом случае всё не настолько абстрактно, как хотелось бы. Если вы инициализируете в PageSize только поле Orient, то ничего не изменится. Width и Height тоже нужно менять.

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

sectionProperties.AddChild(new PageMargin()
{
  Top = 720,
  Right = Convert.ToUInt32(1440.0),
  Bottom = 360,
  Left = Convert.ToUInt32(1440.0),
  Header = (UInt32Value)450U,
  Footer = (UInt32Value)720U,
  Gutter = (UInt32Value)0U
});

Гиперссылки

Interop.Word:

public static void AddHyperlinkedText(MicrosoftWord.Document doc,
                                      string text,
                                      string url)
{
  MicrosoftWord.Range wrdRng = doc.Bookmarks
                                  .get_Item("\\endofdoc")
                                  .Range;
  doc.Hyperlinks.Add(wrdRng, url, TextToDisplay: text);
}

Тут всё просто: как обычно, получаем нужный Range и добавляем гиперссылку. У метода Add много параметров, и можно сконструировать более сложную ссылку.

Open XML:

public static void AddHyperlinkedText(WordprocessingDocument doc,
                                      string text,
                                      string url)
{
  MainDocumentPart mainPart = doc.MainDocumentPart;
  Body body = mainPart.Document.Body;
  Paragraph paragraph = body.AppendChild(new Paragraph());

  var rel = mainPart.AddHyperlinkRelationship(new Uri(url), true);

  Hyperlink hyperlink = new Hyperlink(new Run(
                                    new RunProperties(
                                      new RunStyle 
                                      {
                                        Val = "Hyperlink",
                                      },
                                      new Underline
                                      {
                                        Val = UnderlineValues.Single
                                      },
                                      new Color
                                      {
                                        ThemeColor = ThemeColorValues.Hyperlink
                                      }),
                                      new Text
                                      {
                                        Text = text
                                      })) 
                    {
                      Id = rel.Id 
                    };

  paragraph.AppendChild(hyperlink);
}

Из существенных отличий тут то, что url нужно сначала обернуть в Uri и создать связь url с гиперссылкой через AddHyperlinkRelationship. Потом при создании самой гиперссылки, нужно присвоить полю Id новой гиперссылки Id созданной ранее связи.

Картинки

Interop.Word:

public static void InsertWordPicture(MicrosoftWord.Document doc,
                                     string picturePath)
{
  MicrosoftWord.Range wrdRng = doc.Bookmarks.get_Item("\\endofdoc")
                                            .Range;
  wrdRng.InlineShapes.AddPicture(picturePath);
}

Тут всё достаточно просто, а с Open XML всё оказалось крайне сложно.

Open XML:

Для добавления картинки необходимо соблюсти сложную иерархию объектов с определёнными параметрами. Хорошо, что есть документация на этот счёт. Поэтому пропустим код, требуемый для добавления картинки в этой статье. Разберём ещё один момент, который почему-то не упоминается в документации. Можете заметить, что в том коде нигде не передаётся размер картинки. Фиксируется её размер тут:

new DW.Extent() { Cx = 990000L, Cy = 792000L }

и тут

new A.Extents() { Cx = 990000L, Cy = 792000L }

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

Дело в том, что масштаб отображения картинки здесь завязан на такую вещь, как EMU (English Metric Units).

Для того чтобы передавать методу добавления картинки привычные размеры в пикселях, потребуется следующее преобразование:

double englishMetricUnitsPerInch = 914400;
double pixelsPerInch = 96;
double englishMetricUnitsPerPixel = englishMetricUnitsPerInch / pixelsPerInch;

double emuWidth = width * englishMetricUnitsPerPixel;
double emuHeight = height * englishMetricUnitsPerPixel;

Тут мы получаем количество EMU на пиксель, приняв значение PPI за 96, и умножаем полученное значение на нужное количество пикселей для ширины и высоты. В итоге у наc есть нужная нам ширина и высота в EMU. Их мы и передаём как Cx и Cy для Extent и Extents:

Cx = (Int64Value)emuWidth, Cy = (Int64Value)emuHeight

Таблицы

Interop.Word:

Генерация таблицы через Interop.Word достаточно прямолинейна. Разберём пример, как можно было бы вставить таблицу из квадратной матрицы строк.

public static void InsertWordTable(MicrosoftWord.Document doc,
                                   string[,] table)
{
  MicrosoftWord.Table oTable;
  MicrosoftWord.Range wrdRng = doc.Bookmarks
                                  .get_Item("\\endofdoc")
                                  .Range;

  int rowCount = table.GetLength(0);
  int columnCount = table.GetLength(1);

  oTable = doc.Tables.Add(wrdRng,
                    rowCount,
                    columnCount,
                    DefaultTableBehavior: MicrosoftWord.WdDefaultTableBehavior
                                                       .wdWord9TableBehavior,
                    AutoFitBehavior: MicrosoftWord.WdAutoFitBehavior
                                                  .wdAutoFitWindow);

  for (int i = 0; i < rowCount; i++)
    for (int j = 0; j < columnCount; j++)
      oTable.Cell(i + 1, j + 1).Range.Text = table[i,j];
}

Параметры метода AddDefaultTableBehavior и AutoFitBehavior — как видно из их названия, отвечают за поведение таблицы при необходимости изменения размера под содержимое ячеек. Им присваиваются значения перечислений WdDefaultTableBehavior и WdAutoFitBehavior соответственно. Сам метод Add создаёт в документе таблицу с нужными нам параметрами.

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

oTable.set_Style("Grid Table 4 - Accent 1");

Также для красивого выделения первого столбика, если он является заголовочным, можно присвоить true полю oTable.ApplyStyleFirstColumn.

Расстояние между параграфами текста контролируется через oTable.Range.ParagraphFormat.SpaceAfter. Для компактного отображения таблицы можно использовать

oTable.Range.ParagraphFormat.SpaceAfter = 0;

Также можно устанавливать тип написания текста к строкам или колонкам:

oTable.Rows[1].Range.Font.Bold = 1;
oTable.Column[1].Range.Font.Italic = 1;

Используя эти возможности, можно получить вот такую таблицу:

Open XML:

public static void InsertWordTable(WordprocessingDocument doc,
                                   string[,] table)
{
  DocumentFormat.OpenXml.Wordprocessing.Table dTable =
    new DocumentFormat.OpenXml.Wordprocessing.Table();

  TableProperties props = new TableProperties();

  dTable.AppendChild<TableProperties>(props);

  for (int i = 0; i < table.GetLength(0); i++)
  {
    var tr = new TableRow();

    for (int j = 0; j < table.GetLength(1); j++)
    {
      var tc = new TableCell();
      tc.Append(new Paragraph(new Run(new Text(table[i, j]))));

      tc.Append(new TableCellProperties());

      tr.Append(tc);
    }
    dTable.Append(tr);
  }
  doc.MainDocumentPart.Document.Body.Append(dTable);
}

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

Поэтому при проходе по матрице мы для каждой строки создаём TableRow, а потом для каждого элемента в строке создаём TableCell, куда добавляем новые Paragraph, Run и Text с соответствующим значением из матрицы. TableCellProperties лучше также добавить сразу, чем потом при дальнейшей работе с таблицей наткнуться на System.NullReferenceException при попытке добавить свойство ячейке.

Если мы не зададим в TableProperties ни стиля, ни Borders, то таблица будет выглядеть вот так

Границы таблицы формируются через TableBorders.

var borderValues = new EnumValue<BorderValues>(BorderValues.Single);
var tableBorders = new TableBorders( 
                     new TopBorder { Val = borderValues, Size = 4 },
                     new BottomBorder {  Val = borderValues,  Size = 4 },
                     new LeftBorder { Val = borderValues, Size = 4 },
                     new RightBorder { Val = borderValues, Size = 4 },
                     new InsideHorizontalBorder { Val= borderValues, Size = 4 },
                     new InsideVerticalBorder { Val= borderValues, Size = 4 }));

Перечисление BorderValues здесь задаёт стиль границ.

TableBorders нужно добавить к TableProperties через

props.Append(tableBorders);

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

Задаётся стиль достаточно просто:

TableStyle tableStyle = new TableStyle()
                        {
                          Val = "GridTable4-Accent5"
                        };

Его так же, как и границы, нужно добавить к TableProperties:

props.Append(tableStyle);

Для того чтобы таблица заняла всю ширину страницы можно использовать TableWidth заданную следующим образом:

var tableWidth = new TableWidth()
                 {
                   Width = "5000",
                   Type = TableWidthUnitValues.Pct
                 };

Значение 5000 тут взято "не из воздуха". Тип единицы ширины здесь мы задаём TableWidthUnitValues.Pct – единицы ширины в одну пятидесятую процента страницы или 0,02%. В итоге пять тысяч Pct это 100% ширины страницы.

Этот параметр добавляется к TableProperties аналогичным образом:

props.Append(tableWidth);

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

dTable.PrependChild<TableProperties>(props);

Раскраска таблиц

Для формирования нашего отчёта нам нужно было раскрасить ячейки в некоторых таблицах документа.

Interop.Word:

oTable.Cell(i, j).Range.Shading.BackgroundPatternColor = MicrosoftWord.WdColor
                                                                    .wdColorRed;

где oTable – это созданная нами ранее таблица, i и j — это индексы нужной ячейки. Присваиваемое значение – перечисление WdColor.

Open XML:

tc.Append(new TableCellProperties(
            new Shading { Fill = "FF0000" }));

где tc – это TableCell, с которой идёт работа. Полю Fill присваивается строка с Hex-значением цвета.

Разрыв страницы

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

Interop.Word:

public static void InsertWordBreak(MicrosoftWord.Document doc)
{
  MicrosoftWord.Range wrdRng = doc.Bookmarks.get_Item("\\endofdoc")
                                            .Range;
  wrdRng.InsertBreak();
}

Open XML:

public static void InsertWordBreak(WordprocessingDocument doc)
{
  MainDocumentPart mainPart = doc.MainDocumentPart;
  mainPart.Document.Body.InsertAfter(new Paragraph(
                                       new Run(
                                         new Break()
                                         { 
                                           Type = BreakValues.Page
                                         })),
                                     mainPart.Document.Body.LastChild);
}

Тип разрыва меняется через перечисление BreakValues.

Footer/Header

Также нам нужны были футеры/хедеры в документе.

Interop.Word:

public static void InsertWordFooter(
  MicrosoftWord.Document doc,
  string headerText)
{
  MicrosoftWord.Range headerRange = doc.Sections
                                 .Last
                                 .Headers[MicrosoftWord.WdHeaderFooterIndex
                                                       .wdHeaderFooterPrimary]
                                 .Range;

  headerRange.Fields.Add(headerRange, MicrosoftWord.WdFieldType.wdFieldPage);
  headerRange.Text = headerText;
}

Через headerRange.Font можно поменять параметры текста, например размер, шрифт, цвет и т.д. А headerRange.ParagraphFormat.Alignment, как следует из названия, задаёт выравнивание текста. Это поле принимает значения WdParagraphAlignment.

Open XML:

Тут сложность состоит в том, что футер/хэдер сам по себе хранится в отдельном .xml файлике. Поэтому нам нужно связать хэдер/футер с содержанием документа через SectionProperties.

static void InsertWordHeader(HeaderPart part,
                             string headerText)
{
  MainDocumentPart mainPart = doc.MainDocumentPart;

  if (mainPart.HeaderParts.Any())
    return;

  HeaderPart headerPart = mainPart.AddNewPart<HeaderPart>();

  string headerPartId = mainPart.GetIdOfPart(headerPart);

  part.Header = new Header(
                  new Paragraph(
                    new ParagraphProperties(
                      new ParagraphStyleId() { Val = "Header" }),
                      new Run( new Text() { Text = headerText })));

  var sectionProperties = mainPart.Document
                                  .Body
                                  .GetFirstChild<SectionProperties>();
  sectionProperties.PrependChild<HeaderReference>(new HeaderReference()
                                                  {
                                                    Id = headerPartId
                                                  });
}

Если нужно, чтобы текст перезаписывался на новый при вызове метода добавления хедера, то вместо

if (mainPart.HeaderParts.Any())
  return;

можно использовать

mainDocumentPart.DeleteParts(mainDocumentPart.HeaderParts);

Для футера нужно будет передать mainDocumentPart.FooterParts.

Заключение

Описанные методы работы с Open XML SDK можно собрать в библиотеку классов для внутреннего использования в компании, что мы и сделали. Создание Word документов стало даже удобнее, чем было с Word Interop API.

Здесь может возникнуть закономерный вопрос, есть ли готовые библиотеки на основе Open XML SDK для упрощённой работы с документами? Ответ – однозначно да. Но, к сожалению, поддержка таких библиотек быстро прекращается. Истории создания таких проектов все одинаковые: программисты начинают работать с Word, осознают неудобство существующей инфраструктуры, дорабатывают её — и некоторые библиотеки публикуются на GitHub. Даже если удастся найти относительно свежую версию подобной библиотеки, то, скорее всего, она была реализована под задачи конкретного проекта, и в вашем проекте всё равно будет неудобной в использовании. Плюс появится риск остаться с неподдерживаемой библиотекой.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Victoria Khanieva, Svyatoslav Razmyslov. Tutorial: how to port a project from Interop Word API to Open XML SDK.

Комментарии (3)


  1. skotjuko
    21.08.2021 10:57

    Бог с ним с Вордом, а вот как выкручиваться с Outlook-ом? Куча софта шлет через Interop письма.


    1. SvyatoslavMC
      21.08.2021 11:03

      Microsoft многократно отвечал на этот вопрос, не уточнаяя конкретные сферы разработки - используйте .NET Framework 4.8, он будет поддерживаться.

      Но программисты не сдатся. Возможно, Вам будет интересен тикет: Add support for .NET Core and .NET Framework side-by-side in same process.


      1. skotjuko
        21.08.2021 11:53

        Хотелось бы в светлый новый мир .NET Core, а не зависнуть на клиентских приложениях навсегда на .NET Framework с с# 7 только из-за того, что мейлик не послать.