Search code examples
c#openxmldocxopenxml-sdkopenxml-powertools

How do you create multi-level ordered lists with Open XML in ASP.net?


I've spent countless hours trying to understand ordered lists in Open XML. Here's one of many references.

I found this incredibly helpful example of a simple document creator here.

Also if I may gripe a little bit, I must say this is a painful learning curve. Creating numbering properties and referencing the correct abstractNumberId and the list goes on and on.

Does anyone have a complete example of creating multi-level lists in code? I can do so with no custom settings, as in setting the NumberFormatValues.Decimal. As soon as you want to start controlling the listType you have a lot of instant overhead.

Taking the above example that I started with, I added a level argument:

public void AddBulletList(List<Run> runList, int level)

I incorporate it on this line:

var abstractLevel = new Level(new NumberingFormat() {
    Val = NumberFormatValues.Decimal}, new LevelText() {Val = "·"}) {LevelIndex = level};

Notice that I changed the format type to decimal... the method name says bullet but I'm just testing here.

I also leverage it to handle the indentation:

var indentation = new Indentation() { Left = (720 * (level + 1)).ToString(), Hanging = "360" };  

So in my tests, I send a few sentences to the method passing in 0 for level. Then I send a few more passing in 1 for level.

Two issues with my results:

1) I can't figure out how to reset the counter, so I get this:

1. Sentence 1
2. Sentence 2
3. Sentence 3
    4. Sentence 1
    5. Sentence 2
    6. Sentence 3

I attempted to use the levelRestart but that didn't work:

abstractLevel.LevelRestart = new LevelRestart(){ Val = 0 } // tried 1 also

The only way I could get it to restart was to insert a blank paragraph before inserting the second list of sentences but this has styling issues (spacing).

The second issue that I'm having is the numbering appears in WordDoc, but it shows as bullet points in Microsoft Word. In addition, I get a compatibility mode warning in Microsoft Word.

Someone will come along and say:

Use Open XML Productivity Tools and create a doc and look at the generated code

Well my response to that is:

My eyes are bleeding after looking at the 5,000 lines of code a tiny little document to test with creates.

So I feel like I'm super close. I have a lot of other customizations in my code which is why I continue to reference where I started. If someone could take that example or provide an existing one, I simply want to create multi-level lists and control the type of numbering (lowerRoman, decimal, etc.) is used.

Update 1

I've really just had to bite the bullet and do some serious studying. With Thomas' help I've been able to press forward, but it seems I have one more pesky issue. My new ordered lists do not start at "1".

Notice how in the image, if I click on the level for the first list, it highlights only that list. I expect them to be separate but apparently not. The next list should start at 1.

new list not restarting

For each new ordered list, I'm assigning it a new numberId so I don't know what's happening. Here's the generated markup:

<w:numbering xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se w16cid wp14">
      <w:abstractNum w:abstractNumId="1" w15:restartNumberingAfterBreak="1">
        <w:nsid w:val="191025D9" />
        <w:multiLevelType w:val="hybridMultilevel" />
        <w:tmpl w:val="48A2E570" />
        <w:lvl w:ilvl="0" w:tplc="0409000F">
          <w:start w:val="1" />
          <w:numFmt w:val="decimal" />
          <w:lvlText w:val="%1." />
          <w:lvlJc w:val="left" />
          <w:pPr>
            <w:ind w:start="720" w:hanging="360" />
          </w:pPr>
        </w:lvl>
        <w:lvl w:ilvl="1" w:tplc="04090019">
          <w:start w:val="1" />
          <w:numFmt w:val="lowerLetter" />
          <w:lvlText w:val="%2." />
          <w:lvlJc w:val="left" />
          <w:pPr>
            <w:ind w:start="1440" w:hanging="360" />
          </w:pPr>
        </w:lvl>        
      </w:abstractNum>
      <w:abstractNum w:abstractNumId="2" w15:restartNumberingAfterBreak="1">
        <w:nsid w:val="191025D9" />
        <w:multiLevelType w:val="hybridMultilevel" />
        <w:tmpl w:val="48A2E570" />
        <w:lvl w:ilvl="0" w:tplc="0409000F">
          <w:start w:val="1" />
          <w:numFmt w:val="decimal" />
          <w:lvlText w:val="%1." />
          <w:lvlJc w:val="left" />
          <w:pPr>
            <w:ind w:start="720" w:hanging="360" />
          </w:pPr>
        </w:lvl>
        <w:lvl w:ilvl="1" w:tplc="04090019">
          <w:start w:val="1" />
          <w:numFmt w:val="lowerLetter" />
          <w:lvlText w:val="%2." />
          <w:lvlJc w:val="left" />
          <w:pPr>
            <w:ind w:start="1440" w:hanging="360" />
          </w:pPr>
        </w:lvl>        
      </w:abstractNum>
      <w:num w:numId="1">
        <w:abstractNumId w:val="1" />
      </w:num>
      <w:num w:numId="2">
        <w:abstractNumId w:val="2" />
      </w:num>
    </w:numbering>

Here's the body:

    <w:body xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:p>
    <w:pPr>
      <w:pStyle w:val="ListParagraph" />
      <w:numPr>
        <w:ilvl w:val="0" />
        <w:numId w:val="1" />
      </w:numPr>
    </w:pPr>
    <w:r>
      <w:t>List one item 1</w:t>
    </w:r>
  </w:p>
  <w:p>
    <w:pPr>
      <w:pStyle w:val="ListParagraph" />
      <w:numPr>
        <w:ilvl w:val="0" />
        <w:numId w:val="1" />
      </w:numPr>
    </w:pPr>
    <w:r>
      <w:t>List one item 2</w:t>
    </w:r>
  </w:p>
  <w:p>
    <w:pPr>
      <w:pStyle w:val="ListParagraph" />
      <w:numPr>
        <w:ilvl w:val="0" />
        <w:numId w:val="2" />
      </w:numPr>
    </w:pPr>
    <w:r>
      <w:t>List two item 1</w:t>
    </w:r>
  </w:p>
  <w:p>
    <w:pPr>
      <w:pStyle w:val="ListParagraph" />
      <w:numPr>
        <w:ilvl w:val="0" />
        <w:numId w:val="2" />
      </w:numPr>
    </w:pPr>
    <w:r>
      <w:t>List two item 2</w:t>
    </w:r>
  </w:p>
</w:body>

Solution

  • While I've never used the Open XML Productivity Tools, I regularly use and recommend the Open XML Package Editor for Modern Visual Studios. With that package editor, you can look at the Open XML markup created by Microsoft Word to learn from that.

    So, to answer your question, I've done just that, using a Word template that contains several multi-level lists that behave exactly like you want them to behave (I hope).

    (a)  First paragraph, on outline level 0 (shown as 1 in Word)
    (b)  Second paragraph, on outline level 0
         (1) Third paragraph, on outline level 1 (shown as 2 in Word)
         (2) Fourth paragraph, on outline level 1
    

    Note that you would typically have different numbering formats (e.g., lower letter, upper letter, lower roman, upper roman, decimal) on the different levels of your multi-level list.

    Next, this is the corresponding Open XML markup for the Main Document Part:

      <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
        <w:body>
          <w:p>
            <w:pPr>
              <w:pStyle w:val="ListLowerLetter0"/>
            </w:pPr>
            <w:r>
              <w:t>First paragraph, on outline level 0 (shown as 1 in Word)</w:t>
            </w:r>
          </w:p>
          <w:p>
            <w:pPr>
              <w:pStyle w:val="ListLowerLetter0"/>
            </w:pPr>
            <w:r>
              <w:t>Second paragraph, on outline level 0</w:t>
            </w:r>
          </w:p>
          <w:p>
            <w:pPr>
              <w:pStyle w:val="ListLowerLetter0"/>
              <w:numPr>
                <w:ilvl w:val="1"/>    <!-- This overrides the numbering level -->
                <w:numId w:val="43"/>  <!-- This references the w:numbering/w:num -->
              </w:numPr>
            </w:pPr>
            <w:r>
              <w:t>Third paragraph, on outline level 1 (shown as 2 in Word)</w:t>
            </w:r>
          </w:p>
          <w:p>
            <w:pPr>
              <w:pStyle w:val="ListLowerLetter0"/>
              <w:numPr>
                <w:ilvl w:val="1"/>
                <w:numId w:val="43"/>
              </w:numPr>
            </w:pPr>
            <w:r>
              <w:t>Fourth paragraph, on outline level 1</w:t>
            </w:r>
          </w:p>
        </w:body>
      </w:document>
    

    Note the w:pStyle and w:numPr elements, which specify the numbered paragraph style to be used and, where w:numPr is also used, override the numbering defaults specified in the style.

    Next, this is the Open XML markup for the Style Definitions Part:

      <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
    
        <!-- This is the paragraph style used in the main document part (w:document) -->
        <w:style w:type="paragraph" w:customStyle="1" w:styleId="ListLowerLetter0">
          <w:name w:val="List Lower Letter 0"/>
          <w:basedOn w:val="Normal"/>
          <w:pPr>
            <w:numPr>
              <w:numId w:val="43"/>   <!-- This references the w:numbering/w:num -->
            </w:numPr>
          </w:pPr>
        </w:style>
    
        <!-- This is the list style referenced in the numbering definitions part (w:numbering).
             This is optional but helps if you want to use the list in Word. -->
        <w:style w:type="numbering" w:customStyle="1" w:styleId="ListLowerLetter0List">
          <w:name w:val="List Lower Letter 0 List"/>
          <w:basedOn w:val="NoList"/>
          <w:pPr>
            <w:numPr>
              <w:numId w:val="43"/>   <!-- This references the w:numbering/w:num -->
            </w:numPr>
          </w:pPr>
        </w:style>
    
    
        <!-- I've included this for completeness because it is referenced by our
             paragraph style below -->
        <w:style w:type="paragraph" w:default="1" w:styleId="Normal">
          <w:name w:val="Normal"/>
          <w:rPr>
            <w:kern w:val="16"/>
          </w:rPr>
        </w:style>
    
        <!-- I've included this for completeness because it is referenced by our
             list style below -->
        <w:style w:type="numbering" w:default="1" w:styleId="NoList">
          <w:name w:val="No List"/>
          <w:semiHidden/>
          <w:unhideWhenUsed/>
        </w:style>
      </w:styles>
    

    I would only really need the ListLowerLetter0 style. ListLowerLetter0List is an optional list style. I always use those in my templates. The other two styles are just there for completeness and consistency. Obviously, a real-life Style Definitions Part contains many more styles and other elements.

    Finally, we have the Open XML markup of the Numbering Definitions Part (again with comments explaining what the relevant elements do):

      <w:numbering xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                   xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
                   xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
                   xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
                   mc:Ignorable="w14 w15">
    
        <!-- Here's an example multi-level list -->
        <w:abstractNum w:abstractNumId="67" w15:restartNumberingAfterBreak="0">
          <w:nsid w:val="3E434843" />
          <w:multiLevelType w:val="multilevel" />
          <w:tmpl w:val="1146F302" />
    
          <!-- The w:styleLink references our list style. This is optional (but I use
               it as a best practice in Word) -->
          <w:styleLink w:val="ListLowerLetter0List" />
    
          <!-- This defines the first outline level, i.e., 0 in Open XML lingo or 1
               when you look at it in Word -->
          <w:lvl w:ilvl="0">
            <!-- This starts the level at the ordinal number 1, i.e., "a" on this level -->
            <w:start w:val="1" />
    
            <!-- This defines the number format on this level -->
            <w:numFmt w:val="lowerLetter" />
    
            <!-- This references our paragraph style, which will be the same on each level -->
            <w:pStyle w:val="ListLowerLetter0" />
    
            <!-- This defines the level text, e.g., (a), (b), (c), ... -->
            <w:lvlText w:val="(%1)" />
    
            <!-- The next elements define alignment, indentation, and color -->
            <w:lvlJc w:val="left" />
            <w:pPr>
              <w:ind w:left="851" w:hanging="851" />
            </w:pPr>
            <w:rPr>
              <w:color w:val="auto" />
            </w:rPr>
          </w:lvl>
    
          <!-- This and the following w:lvl elements define levels 1 to 8 (i.e., 2 to 9 in Word) -->
          <w:lvl w:ilvl="1">
            <w:start w:val="1" />
            <w:numFmt w:val="decimal" />
            <w:lvlText w:val="(%2)" />
            <w:lvlJc w:val="left" />
            <w:pPr>
              <w:ind w:left="1418" w:hanging="567" />
            </w:pPr>
            <w:rPr>
              <w:color w:val="auto" />
            </w:rPr>
          </w:lvl>
          <w:lvl w:ilvl="2">
            <w:start w:val="1" />
            <w:numFmt w:val="upperLetter" />
            <w:lvlText w:val="(%3)" />
            <w:lvlJc w:val="left" />
            <w:pPr>
              <w:ind w:left="1985" w:hanging="567" />
            </w:pPr>
            <w:rPr>
              <w:color w:val="auto" />
            </w:rPr>
          </w:lvl>
          <w:lvl w:ilvl="3">
            <w:start w:val="1" />
            <w:numFmt w:val="lowerRoman" />
            <w:lvlText w:val="(%4)" />
            <w:lvlJc w:val="left" />
            <w:pPr>
              <w:ind w:left="2552" w:hanging="567" />
            </w:pPr>
            <w:rPr>
              <w:color w:val="auto" />
            </w:rPr>
          </w:lvl>
          <w:lvl w:ilvl="4">
            <w:start w:val="1" />
            <w:numFmt w:val="lowerLetter" />
            <w:lvlText w:val="%5." />
            <w:lvlJc w:val="left" />
            <w:pPr>
              <w:ind w:left="3119" w:hanging="567" />
            </w:pPr>
            <w:rPr>
              <w:color w:val="auto" />
            </w:rPr>
          </w:lvl>
          <w:lvl w:ilvl="5">
            <w:start w:val="1" />
            <w:numFmt w:val="decimal" />
            <w:lvlText w:val="%6." />
            <w:lvlJc w:val="left" />
            <w:pPr>
              <w:ind w:left="3686" w:hanging="567" />
            </w:pPr>
            <w:rPr>
              <w:color w:val="auto" />
            </w:rPr>
          </w:lvl>
          <w:lvl w:ilvl="6">
            <w:start w:val="1" />
            <w:numFmt w:val="lowerLetter" />
            <w:lvlText w:val="%7." />
            <w:lvlJc w:val="left" />
            <w:pPr>
              <w:ind w:left="4253" w:hanging="567" />
            </w:pPr>
            <w:rPr>
              <w:color w:val="auto" />
            </w:rPr>
          </w:lvl>
          <w:lvl w:ilvl="7">
            <w:start w:val="1" />
            <w:numFmt w:val="lowerRoman" />
            <w:lvlText w:val="%8." />
            <w:lvlJc w:val="left" />
            <w:pPr>
              <w:ind w:left="4820" w:hanging="567" />
            </w:pPr>
            <w:rPr>
              <w:color w:val="auto" />
            </w:rPr>
          </w:lvl>
          <w:lvl w:ilvl="8">
            <w:start w:val="1" />
            <w:numFmt w:val="lowerLetter" />
            <w:lvlText w:val="%9)" />
            <w:lvlJc w:val="left" />
            <w:pPr>
              <w:ind w:left="5387" w:hanging="567" />
            </w:pPr>
            <w:rPr>
              <w:color w:val="auto" />
            </w:rPr>
          </w:lvl>
        </w:abstractNum>
    
        <!-- This is the w:num referenced from our main document part (w:document) -->
        <w:num w:numId="43">
          <w:abstractNumId w:val="67" />
        </w:num>
      </w:numbering>
    

    Now, based on the understanding of the markup required to create the desired effect, it is actually pretty easy to hand-write C# code that produces that markup, using either:

    Let me provide very short examples for both options, producing the following markup:

        <w:num w:numId="43">
          <w:abstractNumId w:val="67" />
        </w:num>
    

    Here's the code that uses the strongly typed classes of the Open XML SDK:

    var num = new NumberingInstance
    {
        NumberID = 43,
        AbstractNumId = new AbstractNumId { Val = 67 }
    };
    

    And here's the code that uses the Open-XML-PowerTools:

    var num =
        new XElement(W.num, new XAttribute(W.numId, 43),
            new XElement(W.abstractNumId, new XAttribute(W.val, 67)));
    

    In this case, the beauty of the Open-XML-PowerTools is that you can literally copy the tag names. Further, the constructors of the Linq to XML classes (e.g., XElement) are much more flexible than those of the Open XML SDK and the C# code looks much like the Open XML markup. Therefore, I have a slight preference for the Open-XML-PowerTools for use cases like this. However, I've very successfully used the Open XML SDK for such cases as well.