You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 18:13:08