OpenXml:工作表子元素在排序中更改导致损坏的文件

时间:2012-08-02 22:44:41

标签: c# openxml openxml-sdk

我正在尝试使用openxml来生成自动excel文件。我面临的一个问题是使用excel的开放式xml对象模型来容纳我的对象模型。我必须明白我为工作表附加子元素的顺序很重要。

例如:

workSheet.Append(sheetViews);
workSheet.Append(columns);
workSheet.Append(sheetData);
workSheet.Append(mergeCells);
workSheet.Append(drawing);

上述排序不会产生任何错误。

但是以下内容:

workSheet.Append(sheetViews);
workSheet.Append(columns);
workSheet.Append(sheetData);
workSheet.Append(drawing);
workSheet.Append(mergeCells);

给出错误

所以这不允许我随时创建一个绘图对象并将其附加到工作表中。这迫使我在使用它们之前创建这些元素。

有人能告诉我,我是否正确理解了这个问题?因为我相信我们应该能够打开任何excel文件,必要时为工作表创建一个新的子元素并附加它。但现在这可能会打破这些元素应该被附加的顺序。

感谢。

5 个答案:

答案 0 :(得分:8)

根据Standard ECMA-376 Office Open XML File FormatsCT_Worksheet具有所需的序列:

CT_Worksheet Schema Diagram

以下内容崩溃的原因:

workSheet.Append(sheetViews);
workSheet.Append(columns);
workSheet.Append(sheetData);
workSheet.Append(drawing);
workSheet.Append(mergeCells);

是因为您之前有drawing mergeCells。只要您在 mergeCells之后追加drawing ,您的代码就可以正常运行。

注意:您可以在ECMA-376 3rd edition Part 1 (.zip) - >中找到完整的XSD。 OfficeOpenXML-XMLSchema-Strict - > sml.xsd。

答案 1 :(得分:1)

我发现对于父对象都有一个Property定义的所有“Singleton”子节点(例如Worksheet.sheetViews),使用singleton属性并将新对象赋值给它而不是使用“Append”这会导致类本身确保订单正确。

workSheet.Append(sheetViews);
workSheet.Append(columns);
workSheet.Append(sheetData);  // bad idea(though it does work if the order is good)
workSheet.Append(drawing);
workSheet.Append(mergeCells);

更正确的格式......

workSheet.sheetViews=sheetViews; // order doesn't matter.
workSheet.columns=columns;
...

答案 2 :(得分:1)

对于那些像我一样通过 Google 到达这里的人,下面的函数解决了插入子元素后的排序问题:

public static T ReorderChildren<T>(T element) where T : OpenXmlElement
{
  Dictionary<Type, int> childOrderHashTable = element.GetType()
                                                  .GetCustomAttributes()
                                                  .Where(x => x is ChildElementInfoAttribute)
                                                  .Select( (x, idx) => new KeyValuePair<Type, int>(((ChildElementInfoAttribute)x).ElementType, idx))
                                                  .ToDictionary(x => x.Key, x => x.Value);

  List<OpenXmlElement> reorderedChildren = element.ChildElements
                                                .OrderBy(x => childOrderHashTable[x.GetType()])
                                                .ToList();
  element.RemoveAllChildren();
  element.Append(reorderedChildren);
  return element;         
}

DocumentFormat.OpenXml 库中生成的类型具有可用于反映来自 OOXML 架构的元数据的自定义属性。此解决方案依赖于 System.ReflectionSystem.Linq(即速度不是很快),但无需对字符串列表进行硬编码以正确排序特定类型的子元素。

我在对 ValidationErrorInfo.Node 属性进行验证后使用此函数,并通过引用清理新创建的元素。这样我就不会在整个文档中递归地应用这个方法。

答案 3 :(得分:0)

作为Joe Masilotti already explained,订单在架构中定义。

不幸的是,OpenXML库无法确保基础XML模式所要求的序列化XML中子元素的正确顺序。如果订单不正确,应用程序可能无法成功解析XML。

以下是我在代码中使用的通用解决方案:

private T GetOrCreateWorksheetChildCollection<T>(Spreadsheet.Worksheet worksheet) 
    where T : OpenXmlCompositeElement, new()
{
    T collection = worksheet.GetFirstChild<T>();
    if (collection == null)
    {
        collection = new T();
        if (!worksheet.HasChildren)
        {
            worksheet.AppendChild(collection);
        }
        else
        {
            // compute the positions of all child elements (existing + new collection)
            List<int> schemaPositions = worksheet.ChildElements
                .Select(e => _childElementNames.IndexOf(e.LocalName)).ToList();
            int collectionSchemaPos = _childElementNames.IndexOf(collection.LocalName);
            schemaPositions.Add(collectionSchemaPos);
            schemaPositions = schemaPositions.OrderBy(i => i).ToList();

            // now get the index where the position of the new child is
            int index = schemaPositions.IndexOf(collectionSchemaPos);

            // this is the index to insert the new element
            worksheet.InsertAt(collection, index);
        }
    }
    return collection;
}

// names and order of possible child elements according to the openXML schema
private static readonly List<string> _childElementNames = new List<string>() { 
    "sheetPr", "dimension", "sheetViews", "sheetFormatPr", "cols", "sheetData", 
    "sheetCalcPr", "sheetProtection", "protectedRanges", "scenarios", "autoFilter",
    "sortState", "dataConsolidate", "customSheetViews", "mergeCells", "phoneticPr",
    "conditionalFormatting", "dataValidations", "hyperlinks", "printOptions", 
    "pageMargins", "pageSetup", "headerFooter", "rowBreaks", "colBreaks", 
    "customProperties", "cellWatches", "ignoredErrors", "smartTags", "drawing",
    "drawingHF", "picture", "oleObjects", "controls", "webPublishItems", "tableParts",
    "extLst"
};

该方法始终将新的子元素插入到正确的位置,以确保生成的文档有效。

答案 4 :(得分:0)

helb的答案很美-谢谢您,helb。

它有一个轻微的缺点,即不测试子元素的顺序是否已经存在问题。下面的微小修改可确保在添加新元素时没有先前存在的问题(您仍然需要他的_childElementNames,这是无价的),并且效率更高:

    private static int getChildElementOrderIndex(OpenXmlElement collection)
    {
        int orderIndex = _childElementNames.IndexOf(collection.LocalName);
        if( orderIndex < 0)
            throw new InvalidOperationException($"Internal: worksheet part {collection.LocalName} not found");
        return orderIndex;
    }
    private static T GetOrCreateWorksheetChildCollection<T>(Worksheet worksheet) where T : OpenXmlCompositeElement, new()
    {
        T collection = worksheet.GetFirstChild<T>();
        if (collection == null)
        {
            collection = new T();
            if (!worksheet.HasChildren)
            {
                worksheet.AppendChild(collection);
            }
            else
            {
                int collectionSchemaPos = getChildElementOrderIndex(collection);
                int insertPos = 0;
                int lastOrderNum = -1;
                for(int i=0; i<worksheet.ChildElements.Count; ++i)
                {
                    int thisOrderNum = getChildElementOrderIndex(worksheet.ChildElements[i]);
                    if(thisOrderNum<=lastOrderNum)
                        throw new InvalidOperationException($"Internal: worksheet parts {_childElementNames[lastOrderNum]} and {_childElementNames[thisOrderNum]} out of order");
                    lastOrderNum = thisOrderNum;
                    if( thisOrderNum < collectionSchemaPos )
                        ++insertPos;
                }
                // this is the index to insert the new element
                worksheet.InsertAt(collection, insertPos);
            }
        }
        return collection;
    }