Копирование листа в пределах книги

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

Решение

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

  1. Открыть документ электронной таблицы с помощью пакета Open XML SDK.
  2. Получить доступ к главному разделу книги, из которого можно получить доступ к ряду взаимосвязанных разделов, таких как отдельные листы.
  3. Получить доступ к листу, который необходимо скопировать.
  4. Создать клон найденного листа и всех связанных с ним разделов и добавить этот клон и все связанные с ним разделы в книгу.
  5. Выполнить очистку для обеспечения правильной работы таблиц, представлений и пр.
  6. Добавить ссылку на созданный лист в список листов главного раздела книги.
  7. Сохранить изменения в книге.

В моей демонстрации будет использоваться пакет SDK версии 2.

Чтобы более наглядно представить изложенный здесь материал, начнем работать с довольно сложной рабочей книгой, содержащей данные, условное форматирование, фигуру, изображение, таблицу, рисунок SmartArt и диаграмму. Книга включает три листа и имеет следующий вид:

Снимок экрана с примером книги Excel

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

Сравнение методов AddPart<T>() и AddNewPart<T>()

Перед подробным рассмотрением перечисленных выше шагов я хочу рассказать о разнице между двумя методами, представленными в пакете SDK для добавления разделов в пакет Open XML. Метод AddNewPart выполняет следующие действия:

  1. Создает пустой раздел типа T и добавляет его в пакет.
  2. После создания раздела добавляет ссылку из ссылающегося раздела на новый раздел.

Следующим шагом после добавления нового раздела с помощью этого метода обычно является вызов метода FeedData() для направления данных в этот раздел.

Метод AddPart выполняет следующие действия:

  1. Если добавленный раздел еще не включен в пакет, метод добавит этот раздел и все связанные с ним разделы в пакет. Таким образом, если вы добавляете раздел А, а раздел А ссылается на раздел Б, который в свою очередь ссылается на раздел В, то при вызове этого метода, произойдет добавление разделов А, Б и В. Кроме этого, метод обеспечивает сохранность отношений добавляемых разделов. Эта функция чем-то напоминает импорт многоуровневого клона.
  2. Если добавляемый раздел уже присутствует в пакете, метод добавит ссылку из ссылающегося раздела на раздел, уже присутствующий в пакете. Например, разделы А и Б присутствуют в пакете, но раздел А не ссылается на раздел Б. При вызове данного метода добавляется ссылка из раздела А на раздел Б, если это поддерживается форматом Open XML.

Как видно, метод AddPart обладает более широкими возможностями, чем простой метод AddNewPart. Вы поймете его преимущества, когда я покажу, как выполняется создание клона раздела в пределах пакета.

Код

Как описано выше в разделе "Решение", для выполнения первых трех шагов требуется открыть книгу и получить доступ к листу, который необходимо скопировать. Для этого используются следующие фрагменты кода:

static void CopySheet(string filename, string sheetName, string clonedSheetName) { //Open workbook using (SpreadsheetDocument mySpreadsheet = SpreadsheetDocument.Open(filename, true)) { WorkbookPart workbookPart = mySpreadsheet.WorkbookPart; //Get the source sheet to be copied WorksheetPart sourceSheetPart = GetWorkSheetPart(workbookPart, sheetName); ... } }

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

static WorksheetPart GetWorkSheetPart(WorkbookPart workbookPart, string sheetName) { //Get the relationship id of the sheetname string relId = workbookPart.Workbook.Descendants<Sheet>() .Where(s => s.Name.Value.Equals(sheetName)) .First() .Id; return (WorksheetPart)workbookPart.GetPartById(relId); }

Теперь, когда мы получили доступ к разделу листа, который необходимо скопировать, надо выполнить задачу создания клона. Именно здесь я и воспользуюсь преимуществом метода AddPart. Возможно, в будущей сборке пакета SDK будет включен метод клонирования для разделов. Как уже было сказано выше, метод AddPart отлично подходит для добавления раздела и всех разделов, на которые он ссылается. К сожалению, эта функция работает только при добавлении разделов, которые еще не присутствуют в пакете. Для обхода этого ограничения мы можем просто вызвать метод AddPart для временной книги, а затем снова вызвать метод AddPart в главной книге. Эта задача выполняется с помощью следующего кода:

static void CopySheet(string filename, string sheetName, string clonedSheetName) { ... //Take advantage of AddPart for deep cloning SpreadsheetDocument tempSheet = SpreadsheetDocument.Create(new MemoryStream(), mySpreadsheet.DocumentType); WorkbookPart tempWorkbookPart = tempSheet.AddWorkbookPart(); WorksheetPart tempWorksheetPart = tempWorkbookPart.AddPart<WorksheetPart>(sourceSheetPart); //Add cloned sheet and all associated parts to workbook WorksheetPart clonedSheet = workbookPart.AddPart<WorksheetPart>(tempWorksheetPart); ... }

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

Теперь необходимо выполнить некоторые задачи по очистке. Например, SpreadsheetML требует, чтобы у каждой таблицы были уникальное имя и идентификатор. Кроме этого, действительно, должен быть только один лист, установленный как главное представление. В следующем коде показано, как выполнить очистку:

static void CopySheet(string filename, string sheetName, string clonedSheetName) { ... //Table definition parts are somewhat special and need unique ids...so let's make an id based on count int numTableDefParts = sourceSheetPart.GetPartsCountOfType<TableDefinitionPart>(); tableId = numTableDefParts; //Clean up table definition parts (tables need unique ids) if (numTableDefParts != 0) FixupTableParts(clonedSheet, numTableDefParts); //There should only be one sheet that has focus CleanView(clonedSheet); ... }

Очистка представления предполагает удаление ссылок на представление из клонированного листа.

static void CleanView(WorksheetPart worksheetPart) { //There can only be one sheet that has focus SheetViews views = worksheetPart.Worksheet.GetFirstChild<SheetViews>(); if (views != null) { views.Remove(); worksheetPart.Worksheet.Save(); } }

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

static void FixupTableParts(WorksheetPart worksheetPart, int numTableDefParts) { //Every table needs a unique id and name foreach (TableDefinitionPart tableDefPart in worksheetPart.TableDefinitionParts) { tableId++; tableDefPart.Table.Id = (uint)tableId; tableDefPart.Table.DisplayName = "CopiedTable" + tableId; tableDefPart.Table.Name = "CopiedTable" + tableId; tableDefPart.Table.Save(); } }

Все в порядке. Последний шаг — это добавление ссылки на добавленный лист в основной раздел книги с помощью следующего кода:

static void CopySheet(string filename, string sheetName, string clonedSheetName) { ... //Add new sheet to main workbook part Sheets sheets = workbookPart.Workbook.GetFirstChild<Sheets>(); Sheet copiedSheet = new Sheet(); copiedSheet.Name = clonedSheetName; copiedSheet.Id = workbookPart.GetIdOfPart(clonedSheet); copiedSheet.SheetId = (uint)sheets.ChildElements.Count + 1; sheets.Append(copiedSheet); //Save Changes workbookPart.Workbook.Save(); ... }

Заключение

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

Вот снимок экрана с созданной книгой:

Снимок экрана с книгой Excel после выполнения кода

Зияд Раджаби (Zeyad Rajabi)

Это локализованная запись блога. Исходную статью можно найти по адресу https://blogs.msdn.com/brian_jones/archive/2009/02/19/how-to-copy-a-worksheet-within-a-workbook.aspx.