使用OpenXML的ChangeDocumentType()导致Excel文件损坏问题求助
问题描述
我正在编写一个.NET程序集处理Excel报表文件,需要同时支持标准工作簿(.xlsx)和模板文件(.xltx)。编写的核心方法NewReport和SaveReport在不修改Document.DocumentType时运行正常,但启用_contexts[id].Document.ChangeDocumentType(SpreadsheetDocumentType.Workbook)代码行后,从模板转换保存的文件会损坏。已知SpreadsheetDocument.CreateFromTemplate()可处理模板转换,但它仅支持传入文件路径,我需要用流来处理,求解决办法。
核心代码如下:
public int NewReport(string filePath) { if (string.IsNullOrWhiteSpace(filePath)) throw new FileNotFoundException("File path is not valid."); if (!File.Exists(filePath)) throw new FileNotFoundException("File does not exist.", filePath); int id = _nextId++; if(!_contexts.TryGetValue(id, out DocumentContext _value)) { try { _contexts[id] = new DocumentContext { FilePath = filePath, IsProtected = false, IsTemplate = false, CurrentSheet = null, ReportStream = new MemoryStream() }; using (FileStream fileStream = File.OpenRead(filePath)) { fileStream.CopyTo(_contexts[id].ReportStream); _contexts[id].Document = SpreadsheetDocument.Open( _contexts[id].ReportStream, true, new OpenSettings { AutoSave = true } ); _contexts[id].IsTemplate = (_contexts[id].Document.DocumentType == SpreadsheetDocumentType.Template); _contexts[id].ReportStream.Position = 0; _contexts[id].Document.ChangeDocumentType(SpreadsheetDocumentType.Workbook); } // Automatically select the first worksheet if available var sheets = _contexts[id].Document.WorkbookPart.Workbook.Sheets; var firstSheet = sheets?.Elements<Sheet>().FirstOrDefault(); if (firstSheet != null) { _contexts[id].CurrentSheet = firstSheet; } } catch (Exception ex) { _contexts.Remove(id); throw new ArgumentException("Failed to open or create the document.", ex); } } return id; } public void SaveReport(int id, Stream outputStream) { if (!_contexts.TryGetValue(id, out var context)) throw new ArgumentException("Invalid document id."); if (context.ReportStream == null) throw new InvalidOperationException("Report has not been created."); if (outputStream == null) throw new ArgumentException("Output stream cannot be null."); context.Document.WorkbookPart.Workbook.Save(); context.Document.Save(); context.ReportStream.Position = 0; context.ReportStream.CopyTo(outputStream); // Do not close outputStream here }
解决方案
直接调用ChangeDocumentType仅修改了文档类型标识,但模板文件的内部结构(如工作簿属性、内容类型定义)和标准工作簿存在差异,这是导致文件损坏的原因。我们可以手动模拟CreateFromTemplate的转换逻辑,全程基于流实现:
修改后的NewReport方法示例:
public int NewReport(string filePath) { if (string.IsNullOrWhiteSpace(filePath)) throw new FileNotFoundException("File path is not valid."); if (!File.Exists(filePath)) throw new FileNotFoundException("File does not exist.", filePath); int id = _nextId++; if (!_contexts.TryGetValue(id, out DocumentContext _value)) { try { var context = new DocumentContext { FilePath = filePath, IsProtected = false, IsTemplate = false, CurrentSheet = null, ReportStream = new MemoryStream() }; using (var templateStream = File.OpenRead(filePath)) { // 先判断是否为模板文件 using (var tempTemplateDoc = SpreadsheetDocument.Open(templateStream, false)) { context.IsTemplate = tempTemplateDoc.DocumentType == SpreadsheetDocumentType.Template; } // 重置流位置,因为上面的Open操作会移动指针 templateStream.Position = 0; if (context.IsTemplate) { // 从模板创建新的工作簿流 using (var templateDoc = SpreadsheetDocument.Open(templateStream, false)) { // 创建新的工作簿文档到目标流 var workbookDoc = SpreadsheetDocument.Create( context.ReportStream, SpreadsheetDocumentType.Workbook, true ); // 复制模板的所有部件到新工作簿 foreach (var part in templateDoc.Parts) { var newPart = workbookDoc.AddPart(part.OpenXmlPart, part.RelationshipId); // 复制部件内容 using (var partStream = part.OpenXmlPart.GetStream()) using (var newPartStream = newPart.GetStream(FileMode.Create, FileAccess.Write)) { partStream.CopyTo(newPartStream); } } // 修改工作簿属性,移除模板标记 var workbookPart = workbookDoc.WorkbookPart; if (workbookPart.Workbook.WorkbookPr != null) { workbookPart.Workbook.WorkbookPr.Template = null; workbookPart.Workbook.WorkbookPr.DefaultThemeVersion = 164011; } // 修正内容类型:替换模板类型为标准工作簿类型 var contentTypeManager = workbookDoc.ContentTypeManager; var templateContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"; var workbookContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"; contentTypeManager.RenameContentType(templateContentType, workbookContentType); workbookPart.Workbook.Save(); workbookDoc.Save(); // 重置流位置供后续使用 context.ReportStream.Position = 0; context.Document = workbookDoc; } } else { // 处理普通工作簿,沿用原逻辑 templateStream.CopyTo(context.ReportStream); context.ReportStream.Position = 0; context.Document = SpreadsheetDocument.Open( context.ReportStream, true, new OpenSettings { AutoSave = true } ); } } // 自动选择第一个工作表 var sheets = context.Document.WorkbookPart.Workbook.Sheets; var firstSheet = sheets?.Elements<Sheet>().FirstOrDefault(); if (firstSheet != null) { context.CurrentSheet = firstSheet; } _contexts[id] = context; } catch (Exception ex) { _contexts.Remove(id); throw new ArgumentException("Failed to open or create the document.", ex); } } return id; }
关键说明
- 针对模板文件,不直接修改原流,而是创建新的工作簿流并复制模板的所有部件,避免破坏原模板结构
- 移除
WorkbookPr中的模板特有标记,调整主题版本以匹配标准工作簿 - 更新内容类型定义,将模板类型替换为标准工作簿类型
- 普通工作簿仍使用原有逻辑处理,保证兼容性
内容的提问来源于stack exchange,提问作者weirdgyn
相关产品推荐
相关产品推荐

