8

我正在尝试使用 openxml 来生成自动化的 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 文件,如有必要,可以为工作表创建一个新的子元素并附加它。但是现在这可能会破坏这些元素应该被附加的顺序。

谢谢。

4

5 回答 5

9

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

CT_Worksheet 架构图

以下是崩溃的原因:

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

是因为你有drawing mergeCells。只要您附加您的mergeCells after drawing,您的代码应该可以正常工作。

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

于 2012-08-31T19:32:20.703 回答
1

我发现对于父对象定义了属性的所有“单例”子对象(例如 Worksheet.sheetViews),使用单例属性并将新对象分配给该对象而不是使用“追加”这会导致类本身确保顺序是正确的。

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;
...
于 2013-09-18T18:44:42.027 回答
1

正如 Joe Masilotti已经解释的那样,顺序是在模式中定义的。

不幸的是,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"
};

该方法总是在正确的位置插入新的子元素,确保生成的文档是有效的。

于 2016-08-04T06:06:39.693 回答
1

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

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.Reflectionand System.Linq(即,不是很快),但无需硬编码字符串列表以正确排序特定类型的子元素。

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

于 2021-08-02T20:42:55.777 回答
0

helb 的回答很漂亮 - 谢谢你,helb。

它有一个小缺点,它不测试子元素的顺序是否已经存在问题。以下轻微修改确保添加新元素时没有预先存在的问题(您仍然需要 his _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;
    }
于 2020-06-11T03:33:59.727 回答