ms-wordopenxml

Word Open XML Restart the numbering at 1


I am creating a Word document using Open XML, and cannot understand how to make numbering reset.

What I want is this:

1. Top index line A

2. Top index line B

1. Top index line C

But what I'm getting is this:

1. Top index line A

2. Top index line B

3. Top index line C

In my Open Xml I have two AbstractNum objects that define the layout of the numbering. Both AbstractNums are identical, except that one has an abstractNumId of 200 and the other has an abstractNumId of 201.

I have shown only the first level definition for brevity.

<w:abstractNum 
    xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" 
    w:abstractNumId="200" 
    w15:restartNumberingAfterBreak="0"   
    xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:multiLevelType w:val="multilevel" />
  <w:lvl w:ilvl="0">
    <w:start w:val="1" />
    <w:numFmt w:val="decimal" />
    <w:suff w:val="space" />
    <w:lvlText w:val="%1." />
    <w:lvlJc w:val="left" />
    <w:pPr>
      <w:ind w:left="360" w:hanging="360" />
    </w:pPr>
    <w:rPr>
      <w:b />
    </w:rPr>
  </w:lvl>
</w:abstractNum>

I also have two NumberingInstance objects. One has a numbering instance Id of 100 and an AbstractNumId of 200. The other has a numbering instance Id of 101 and an AbstractNumId of 201.

And in the document body, I have three paragraphs, the first two have a numbering instance id of 100, and the last one, 101.

<w:body xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:sectPr>
    <w:pgSz w:w="11932" w:h="16875" w:orient="portrait" />
    <w:pgMar w:top="1443" w:right="1443" w:bottom="1443" w:left="1443" w:header="710" w:footer="710" w:gutter="568" />
  </w:sectPr>
  <w:p>
    <w:pPr>
      <w:pStyle w:val="legalPara" />
      <w:numPr>
        <w:ilvl w:val="0" />
        <w:numId w:val="100" />
      </w:numPr>
    </w:pPr>
    <w:r>
      <w:t xml:space="default">Top index line A</w:t>
    </w:r>
  </w:p>
  <w:p>
    <w:pPr>
      <w:pStyle w:val="legalPara" />
      <w:numPr>
        <w:ilvl w:val="0" />
        <w:numId w:val="100" />
      </w:numPr>
    </w:pPr>
    <w:r>
      <w:t xml:space="default">Top index line B</w:t>
    </w:r>
  </w:p>
  <w:p>
    <w:pPr>
      <w:pStyle w:val="legalPara" />
      <w:numPr>
        <w:ilvl w:val="0" />
        <w:numId w:val="101" />
      </w:numPr>
    </w:pPr>
    <w:r>
      <w:t xml:space="default">Top index line C</w:t>
    </w:r>
  </w:p>
</w:body>

From what I understand, switching the number id on the last paragraph from 100 to 101 should start the numbering again at 1, but it does not.

I have tried correcting my Open XML in Word and identifying the difference in the OpenXML Productivity tool, but Word seems to be doing what I am doing - switching to a different number Id. I am omitting to do something.

Thanks for reading.

Here is a code example that runs in a C# console app.

using System.Text;
using OpenXml = DocumentFormat.OpenXml;
using OpenXmlPackaging = DocumentFormat.OpenXml.Packaging;
using OpenXmlWordProc = DocumentFormat.OpenXml.Wordprocessing;

Console.WriteLine("Application: Started");

//
using (MemoryStream ms = new MemoryStream())
{

    //
    using (OpenXmlPackaging.WordprocessingDocument wordDocument = OpenXmlPackaging.WordprocessingDocument.Create(ms, OpenXml.WordprocessingDocumentType.Document, true))
    {

        // Set up main docjment part
        OpenXmlPackaging.MainDocumentPart mainPart = wordDocument.AddMainDocumentPart();
        {

            // Setup the numbering
            {

                //
                OpenXmlPackaging.NumberingDefinitionsPart numberingDefinitionsPart = mainPart.NumberingDefinitionsPart != null ? mainPart.NumberingDefinitionsPart : mainPart.AddNewPart<OpenXmlPackaging.NumberingDefinitionsPart>();

                //
                if (numberingDefinitionsPart.Numbering is null)
                    numberingDefinitionsPart.Numbering = new OpenXmlWordProc.Numbering();
                ArgumentNullException.ThrowIfNull(numberingDefinitionsPart.Numbering);

            }

        }

        // Set up document
        mainPart.Document = new OpenXmlWordProc.Document();

        // Set up body
        OpenXmlWordProc.Body body = mainPart.Document.AppendChild(new OpenXmlWordProc.Body());


        ///////////////////////////////////////////////////////////////////
        // Add the content

        // For some reason, Word inserts an abstract-numbering with id 0
        //_addAbstractNumbering(mainPart, 0);

        // Create a numbering (id:100) based on an abstract-numbering (id:200)
        _addAbstractNumbering(mainPart, 200);
        _addNumbering(mainPart, iNumberId: 100, iAbstractNumberingId: 200); // <-- Remove these lines and it works!!!

        // Line A
        OpenXmlWordProc.Paragraph P = new OpenXmlWordProc.Paragraph(
            new OpenXmlWordProc.ParagraphProperties()
            {
                NumberingProperties = new OpenXmlWordProc.NumberingProperties()
                {
                    NumberingLevelReference = new OpenXmlWordProc.NumberingLevelReference() { Val = 0 },
                    NumberingId = new OpenXmlWordProc.NumberingId() { Val = 100 }
                }
            },
            new OpenXmlWordProc.Run(
                new OpenXmlWordProc.Text("This is line A")
            )
        );
        body.Append(P);

        // Line B
        P = new OpenXmlWordProc.Paragraph(
            new OpenXmlWordProc.ParagraphProperties()
            {
                NumberingProperties = new OpenXmlWordProc.NumberingProperties()
                {
                    NumberingLevelReference = new OpenXmlWordProc.NumberingLevelReference() { Val = 0 },
                    NumberingId = new OpenXmlWordProc.NumberingId() { Val = 100 }
                }
            },
            new OpenXmlWordProc.Run(
                new OpenXmlWordProc.Text("This is line B")
            )
        );
        body.Append(P);

        // Create a numbering (id:101) based on an abstract-numbering (id:201)
        _addAbstractNumbering(mainPart, 201);
        _addNumbering(mainPart, iNumberId: 101, iAbstractNumberingId: 201);  // <-- Remove these lines and it works!!!

        // Line C
        P = new OpenXmlWordProc.Paragraph(
            new OpenXmlWordProc.ParagraphProperties()
            {
                NumberingProperties = new OpenXmlWordProc.NumberingProperties()
                {
                    NumberingLevelReference = new OpenXmlWordProc.NumberingLevelReference() { Val = 0 },
                    NumberingId = new OpenXmlWordProc.NumberingId() { Val = 101 }
                }
            },
            new OpenXmlWordProc.Run(
                new OpenXmlWordProc.Text("This is line C")
            )
        );
        body.Append(P);

    }

    //
    File.WriteAllBytes("E:\\test-word.docx", ms.ToArray());

}

//
Console.WriteLine("Application: Exit");




/////////////////////////////////////////////////////////////////////
// Functions

void _addAbstractNumbering(OpenXmlPackaging.MainDocumentPart mainPart, int iAbstractNumberingId)
{

    //
    OpenXmlWordProc.Level ___buildLevel(int iLevel)
    {

        //
        int[] IndentationLefts = { 360, 792, 1224, 1728, 2232, 2736 };
        int[] IndentationHangings = { 360, 432, 504, 648, 792, 936 };

        //
        StringBuilder sbFormat = new StringBuilder();
        for (int i1 = 0; i1 <= iLevel; i1++)
            sbFormat.Append($"%{i1 + 1}.");

        //
        OpenXmlWordProc.Level level = new OpenXmlWordProc.Level()
        {
            LevelIndex = iLevel,
            NumberingFormat = new OpenXmlWordProc.NumberingFormat { Val = OpenXmlWordProc.NumberFormatValues.Decimal },
            LevelText = new OpenXmlWordProc.LevelText() { Val = sbFormat.ToString() },
            LevelJustification = new OpenXmlWordProc.LevelJustification() { Val = OpenXmlWordProc.LevelJustificationValues.Left },
            StartNumberingValue = new OpenXmlWordProc.StartNumberingValue() { Val = 1 },
            LevelSuffix = new OpenXmlWordProc.LevelSuffix() { Val = OpenXmlWordProc.LevelSuffixValues.Space },
            PreviousParagraphProperties = new OpenXmlWordProc.PreviousParagraphProperties()
            {
                Indentation = new OpenXmlWordProc.Indentation() { Left = IndentationLefts[iLevel].ToString(), Hanging = IndentationHangings[iLevel].ToString() }
            }
        };

        //
        if (iLevel == 0)
        {

            // Make the top level bold
            level.NumberingSymbolRunProperties = new OpenXmlWordProc.NumberingSymbolRunProperties()
            {
                Bold = new OpenXmlWordProc.Bold()
            };

        }

        //
        return level;

    }


    //
    ArgumentNullException.ThrowIfNull(mainPart.NumberingDefinitionsPart);
    ArgumentNullException.ThrowIfNull(mainPart.NumberingDefinitionsPart.Numbering);

    //
    OpenXmlWordProc.AbstractNum abstractNum = new OpenXmlWordProc.AbstractNum()
    {
        AbstractNumberId = iAbstractNumberingId,
        MultiLevelType = new OpenXmlWordProc.MultiLevelType { Val = OpenXmlWordProc.MultiLevelValues.Multilevel }
    };
    for (int iLevel = 0; iLevel <= 5; iLevel++)
        abstractNum.Append(___buildLevel(iLevel));

    // This line does not seem to make any difference
    //abstractNum.SetAttribute(new OpenXml.OpenXmlAttribute("w15", "restartNumberingAfterBreak", "http://schemas.microsoft.com/office/word/2012/wordml", "0"));

    //
    mainPart.NumberingDefinitionsPart.Numbering.Append(abstractNum);

    //
    Console.WriteLine($"Created AbstractNum with AbstractNumId:{abstractNum.AbstractNumberId}");

}

void _addNumbering(OpenXmlPackaging.MainDocumentPart mainPart, int iNumberId, int iAbstractNumberingId)
{

    //
    ArgumentNullException.ThrowIfNull(mainPart.NumberingDefinitionsPart);
    ArgumentNullException.ThrowIfNull(mainPart.NumberingDefinitionsPart.Numbering);

    //
    OpenXmlWordProc.NumberingInstance numberingInstance = new OpenXmlWordProc.NumberingInstance
    {
        NumberID = iNumberId,
        AbstractNumId = new OpenXmlWordProc.AbstractNumId { Val = iAbstractNumberingId }
    };
    mainPart.NumberingDefinitionsPart.Numbering.Append(numberingInstance);

    //
#if DEBUG
    System.Diagnostics.Trace.WriteLine($"Created NumberingInstance with NumberId:{numberingInstance.NumberID} and AbstractNumId:{numberingInstance.AbstractNumId.Val}");
#endif // #if DEBUG

}

If you want to run this code, you will need to install Microsoft's DocumentFormat.OpenXml in the NuGet package manager.

This code creates a Word document with three lines, all three are numbered. For the last of the three lines, the numbering is changed to force a reset of the numbering, but this does not work.

If I comment out the calls to _addNumbering, the numbering does reset, just not with the numbering layout defined in the abstract numbering.

Here is the replacement function that makes sure the order of items matches that generated by Word:

void _addAbstractNumbering(OpenXmlPackaging.MainDocumentPart mainPart, int iAbstractNumberingId)
{

    //
    OpenXmlWordProc.Level ___buildLevel(int iLevel)
    {

        //
        int[] IndentationLefts = { 360, 792, 1224, 1728, 2232, 2736 };
        int[] IndentationHangings = { 360, 432, 504, 648, 792, 936 };

        //
        StringBuilder sbFormat = new StringBuilder();
        for (int i1 = 0; i1 <= iLevel; i1++)
            sbFormat.Append($"%{i1 + 1}.");

        //
        OpenXmlWordProc.Level level = new OpenXmlWordProc.Level()
        {
            LevelIndex = iLevel,
            NumberingFormat = new OpenXmlWordProc.NumberingFormat { Val = OpenXmlWordProc.NumberFormatValues.Decimal },
            LevelText = new OpenXmlWordProc.LevelText() { Val = sbFormat.ToString() },
            LevelJustification = new OpenXmlWordProc.LevelJustification() { Val = OpenXmlWordProc.LevelJustificationValues.Left },
            StartNumberingValue = new OpenXmlWordProc.StartNumberingValue() { Val = 1 },
            LevelSuffix = new OpenXmlWordProc.LevelSuffix() { Val = OpenXmlWordProc.LevelSuffixValues.Space },
            PreviousParagraphProperties = new OpenXmlWordProc.PreviousParagraphProperties()
            {
                Indentation = new OpenXmlWordProc.Indentation() { Left = IndentationLefts[iLevel].ToString(), Hanging = IndentationHangings[iLevel].ToString() }
            }
        };

        //
        if (iLevel == 0)
        {

            // Make the top level bold
            level.NumberingSymbolRunProperties = new OpenXmlWordProc.NumberingSymbolRunProperties()
            {
                Bold = new OpenXmlWordProc.Bold()
            };

        }

        //
        return level;

    }


    //
    ArgumentNullException.ThrowIfNull(mainPart.NumberingDefinitionsPart);
    ArgumentNullException.ThrowIfNull(mainPart.NumberingDefinitionsPart.Numbering);

    //
    OpenXmlWordProc.AbstractNum abstractNum = new OpenXmlWordProc.AbstractNum()
    {
        AbstractNumberId = iAbstractNumberingId,
        MultiLevelType = new OpenXmlWordProc.MultiLevelType { Val = OpenXmlWordProc.MultiLevelValues.Multilevel }
    };
    for (int iLevel = 0; iLevel <= 5; iLevel++)
        abstractNum.Append(___buildLevel(iLevel));

    // This line does not seem to make any difference
    //abstractNum.SetAttribute(new OpenXml.OpenXmlAttribute("w15", "restartNumberingAfterBreak", "http://schemas.microsoft.com/office/word/2012/wordml", "0"));

    // * * *
    // This is the correction to the question.
    // The AbstractNum objects MUST be inserted BEFORE the NumberingInstances
    // * * *
    OpenXmlWordProc.AbstractNum? lastFound = mainPart.NumberingDefinitionsPart.Numbering.OfType<OpenXmlWordProc.AbstractNum>().LastOrDefault();
    if (lastFound != null)
        mainPart.NumberingDefinitionsPart.Numbering.InsertAfter(abstractNum, lastFound);
    else if(mainPart.NumberingDefinitionsPart.Numbering.Any())
        mainPart.NumberingDefinitionsPart.Numbering.InsertBefore(abstractNum, mainPart.NumberingDefinitionsPart.Numbering.First());
    else
        mainPart.NumberingDefinitionsPart.Numbering.Append(abstractNum);

    //
    Console.WriteLine($"Created AbstractNum with AbstractNumId:{abstractNum.AbstractNumberId}");

}

Solution

  • The problem seems to be the sequence of the <w:abstractNum> and <w:num> elements in the numbering.xml part.

    I used your code to create a test .docx and it has these elements in this sequence:

    <w:abstractNum w:abstractNumId="200"/>
    <w:num w:numId="100" />
    <w:abstractNum w:abstractNumId="201"/>
    <w:num w:numId="101" />
    

    but it looks as if either the relevant schema or Word requires this sequence.

    <w:abstractNum w:abstractNumId="200"/>
    <w:abstractNum w:abstractNumId="201"/>
    <w:num w:numId="100" />
    <w:num w:numId="101" />
    

    Best to check that yourself in case I have made some other change accidentally.

    As a starting point I would take the view that the schema does not define a sequence, because if so, Word should error on opening the document. But I haven't actually checked.