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

9
我将尝试使用openxml来生成自动化的Excel文件。我面临的一个问题是如何将我的对象模型与Excel的open 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个回答

9
根据标准 ECMA-376 Office Open XML 文件格式CT_Worksheet有一个必需的顺序:

CT_Worksheet Schema Diagram

以下代码崩溃的原因是:
workSheet.Append(sheetViews);
workSheet.Append(columns);
workSheet.Append(sheetData);
workSheet.Append(drawing);
workSheet.Append(mergeCells);

因为您在mergeCells之前使用了drawing。只要将mergeCells添加到drawing之后,您的代码就应该可以正常工作。
注意:您可以在ECMA-376 3rd edition Part 1 (.zip)中找到完整的XSD -> OfficeOpenXML-XMLSchema-Strict -> sml.xsd。

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"
};

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

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.ReflectionSystem.Linq(即不是非常快),但消除了需要硬编码字符串列表以正确排序特定类型的子元素的需要。
我在对ValidationErrorInfo.Node属性进行验证后使用此函数,并通过引用清理新创建的元素。这样,我就不必在整个文档中递归应用此方法。

1
我发现,对于所有父对象定义了属性的“Singleton”子对象(例如 Worksheet.sheetViews),请使用单例属性并将新对象分配给它,而不是使用“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;
...

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;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接