OpenXML 2 SDK - Word document - Create bulleted list programmatically

19,073

Solution 1

Lists in OpenXML are a little confusing.

There is a NumberingDefinitionsPart that describes all of the lists in the document. It contains information on how the lists should appear (bulleted, numbered, etc.) and also assigns and ID to each one.

Then in the MainDocumentPart, for every item in the list you want to create, you add a new paragraph and assign the ID of the list you want to that paragraph.

So to create a bullet list such as:

  • Hello,
  • world!

You would first have to create a NumberingDefinitionsPart:

    NumberingDefinitionsPart numberingPart =
      mainDocumentPart.AddNewPart<NumberingDefinitionsPart>("someUniqueIdHere");

    Numbering element = 
      new Numbering(
        new AbstractNum(
          new Level(
            new NumberingFormat() { Val = NumberFormatValues.Bullet },
            new LevelText() { Val = "·" }
          ) { LevelIndex = 0 }
        ) { AbstractNumberId = 1 },
        new NumberingInstance(
          new AbstractNumId() { Val = 1 }
        ) { NumberID = 1 });

    element.Save(numberingPart);

Then you create the MainDocumentPart as you normally would, except in the paragraph properties, assign the numbering ID:

    MainDocumentPart mainDocumentPart =
      package.AddMainDocumentPart();

    Document element = 
      new Document(
        new Body(
          new Paragraph(
            new ParagraphProperties(
              new NumberingProperties(
                new NumberingLevelReference() { Val = 0 },
                new NumberingId() { Val = 1 })),
            new Run(
              new RunProperties(),
              new Text("Hello, ") { Space = "preserve" })),
          new Paragraph(
            new ParagraphProperties(
              new NumberingProperties(
                new NumberingLevelReference() { Val = 0 },
                new NumberingId() { Val = 1 })),
            new Run(
              new RunProperties(),
              new Text("world!") { Space = "preserve" }))));

    element.Save(mainDocumentPart);

There is a better explanation of the options available in the OpenXML reference guide in Section 2.9.

Solution 2

I wanted something that would allow me to add more than one bullet list to a document. After banging my head against my desk for a while, I managed to combine a bunch of different posts and examine my document with the Open XML SDK 2.0 Productity Tool and figured some stuff out. The document it produces now passes validation for by version 2.0 and 2.5 of the SDK Productivity tool.

Here is the code; hopefully it saves someone some time and aggravation.

Usage:

const string fileToCreate = "C:\\temp\\bulletTest.docx";

 if (File.Exists(fileToCreate))
    File.Delete(fileToCreate);

var writer = new SimpleDocumentWriter();
List<string> fruitList = new List<string>() { "Apple", "Banana", "Carrot"};
writer.AddBulletList(fruitList);
writer.AddParagraph("This is a spacing paragraph 1.");

List<string> animalList = new List<string>() { "Dog", "Cat", "Bear" };
writer.AddBulletList(animalList);
writer.AddParagraph("This is a spacing paragraph 2.");

List<string> stuffList = new List<string>() { "Ball", "Wallet", "Phone" };
writer.AddBulletList(stuffList);
writer.AddParagraph("Done.");

writer.SaveToFile(fileToCreate);

Using statements:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;    

Code

public class SimpleDocumentWriter : IDisposable
{
    private MemoryStream _ms;
    private WordprocessingDocument _wordprocessingDocument;

    public SimpleDocumentWriter()
    {
        _ms = new MemoryStream();
        _wordprocessingDocument = WordprocessingDocument.Create(_ms, WordprocessingDocumentType.Document);
        var mainDocumentPart = _wordprocessingDocument.AddMainDocumentPart();
        Body body = new Body();
        mainDocumentPart.Document = new Document(body);
    }

    public void AddParagraph(string sentence)
    {
        List<Run> runList = ListOfStringToRunList(new List<string> { sentence});
        AddParagraph(runList);
    }
    public void AddParagraph(List<string> sentences)
    {
        List<Run> runList = ListOfStringToRunList(sentences);
        AddParagraph(runList);
    }

    public void AddParagraph(List<Run> runList)
    {
        var para = new Paragraph();
        foreach (Run runItem in runList)
        {
            para.AppendChild(runItem);
        }

        Body body = _wordprocessingDocument.MainDocumentPart.Document.Body;
        body.AppendChild(para);
    }

    public void AddBulletList(List<string> sentences)
    {
        var runList = ListOfStringToRunList(sentences);

        AddBulletList(runList);
    }


    public void AddBulletList(List<Run> runList)
    {
        // Introduce bulleted numbering in case it will be needed at some point
        NumberingDefinitionsPart numberingPart = _wordprocessingDocument.MainDocumentPart.NumberingDefinitionsPart;
        if (numberingPart == null)
        {
            numberingPart = _wordprocessingDocument.MainDocumentPart.AddNewPart<NumberingDefinitionsPart>("NumberingDefinitionsPart001");
            Numbering element = new Numbering();
            element.Save(numberingPart);
        }

        // Insert an AbstractNum into the numbering part numbering list.  The order seems to matter or it will not pass the 
        // Open XML SDK Productity Tools validation test.  AbstractNum comes first and then NumberingInstance and we want to
        // insert this AFTER the last AbstractNum and BEFORE the first NumberingInstance or we will get a validation error.
        var abstractNumberId = numberingPart.Numbering.Elements<AbstractNum>().Count() + 1;
        var abstractLevel = new Level(new NumberingFormat() {Val = NumberFormatValues.Bullet}, new LevelText() {Val = "·"}) {LevelIndex = 0};
        var abstractNum1 = new AbstractNum(abstractLevel) {AbstractNumberId = abstractNumberId};

        if (abstractNumberId == 1)
        {
            numberingPart.Numbering.Append(abstractNum1);
        }
        else
        {
            AbstractNum lastAbstractNum = numberingPart.Numbering.Elements<AbstractNum>().Last();
            numberingPart.Numbering.InsertAfter(abstractNum1, lastAbstractNum);
        }

        // Insert an NumberingInstance into the numbering part numbering list.  The order seems to matter or it will not pass the 
        // Open XML SDK Productity Tools validation test.  AbstractNum comes first and then NumberingInstance and we want to
        // insert this AFTER the last NumberingInstance and AFTER all the AbstractNum entries or we will get a validation error.
        var numberId = numberingPart.Numbering.Elements<NumberingInstance>().Count() + 1;
        NumberingInstance numberingInstance1 = new NumberingInstance() {NumberID = numberId};
        AbstractNumId abstractNumId1 = new AbstractNumId() {Val = abstractNumberId};
        numberingInstance1.Append(abstractNumId1);

        if (numberId == 1)
        {
            numberingPart.Numbering.Append(numberingInstance1);
        }
        else
        {
            var lastNumberingInstance = numberingPart.Numbering.Elements<NumberingInstance>().Last();
            numberingPart.Numbering.InsertAfter(numberingInstance1, lastNumberingInstance);
        }

        Body body = _wordprocessingDocument.MainDocumentPart.Document.Body;

        foreach (Run runItem in runList)
        {
            // Create items for paragraph properties
            var numberingProperties = new NumberingProperties(new NumberingLevelReference() {Val = 0}, new NumberingId() {Val = numberId});
            var spacingBetweenLines1 = new SpacingBetweenLines() { After = "0" };  // Get rid of space between bullets
            var indentation = new Indentation() { Left = "720", Hanging = "360" };  // correct indentation 

            ParagraphMarkRunProperties paragraphMarkRunProperties1 = new ParagraphMarkRunProperties();
            RunFonts runFonts1 = new RunFonts() { Ascii = "Symbol", HighAnsi = "Symbol" };
            paragraphMarkRunProperties1.Append(runFonts1);

            // create paragraph properties
            var paragraphProperties = new ParagraphProperties(numberingProperties, spacingBetweenLines1, indentation, paragraphMarkRunProperties1);

            // Create paragraph 
            var newPara = new Paragraph(paragraphProperties);

            // Add run to the paragraph
            newPara.AppendChild(runItem);

            // Add one bullet item to the body
            body.AppendChild(newPara);
        }
    }


    public void Dispose()
    {
        CloseAndDisposeOfDocument();
        if (_ms != null)
        {
            _ms.Dispose();
            _ms = null;
        }
    }

    public MemoryStream SaveToStream()
    {
        _ms.Position = 0;
        return _ms;
    }

    public void SaveToFile(string fileName)
    {
        if (_wordprocessingDocument != null)
        {
            CloseAndDisposeOfDocument();
        }

        if (_ms == null)
            throw new ArgumentException("This object has already been disposed of so you cannot save it!");

        using (var fs = File.Create(fileName))
        {
            _ms.WriteTo(fs);
        }
    }

    private void CloseAndDisposeOfDocument()
    {
        if (_wordprocessingDocument != null)
        {
            _wordprocessingDocument.Close();
            _wordprocessingDocument.Dispose();
            _wordprocessingDocument = null;
        }
    }

    private static List<Run> ListOfStringToRunList(List<string> sentences)
    {
        var runList = new List<Run>();
        foreach (string item in sentences)
        {
            var newRun = new Run();
            newRun.AppendChild(new Text(item));
            runList.Add(newRun);
        }

        return runList;
    }
}

Solution 3

Adam's answer above is correct except it is new NumberingInstance( instead of new Num( as noted in a comment.

Additionally, if you have multiple lists, you should have multiple Numbering elements (each with it's own id eg 1, 2, 3 etc -- one for each list in the document. This doesn't seem to be a problem with bullet lists, but numbered lists will continue using the same numbering sequence (as opposed to starting over again at 1) because it will think that it's the same list. The NumberingId has to be referenced in your paragraph like this:

ParagraphProperties paragraphProperties1 = new ParagraphProperties();
ParagraphStyleId paragraphStyleId1 = new ParagraphStyleId() { Val = "ListParagraph" };
NumberingProperties numberingProperties1 = new NumberingProperties();
NumberingLevelReference numberingLevelReference1 = new NumberingLevelReference() { Val = 0 };

NumberingId numberingId1 = new NumberingId(){ Val = 1 }; //Val is 1, 2, 3 etc based on your numberingid in your numbering element
numberingProperties1.Append(numberingLevelReference1);
numberingProperties1.Append(numberingId1);
paragraphProperties1.Append(paragraphStyleId1);
paragraphProperties1.Append(numberingProperties1);

Children of the Level element will have an effect on the type of bullet, and the indentation. My bullets were too small until I added this to the Level element:

new NumberingSymbolRunProperties(
    new RunFonts() { Hint = FontTypeHintValues.Default, Ascii = "Symbol", HighAnsi =   "Symbol" })

Indentation was a problem until I added this element to the Level element as well:

new PreviousParagraphProperties(
  new Indentation() { Left = "864", Hanging = "360" })

Solution 4

And if you are like me - creating a document from a template, then you may want to use this code, to handle both situations - when your template does or does not contain any numbering definitions:

// Introduce bulleted numbering in case it will be needed at some point
NumberingDefinitionsPart numberingPart = document.MainDocumentPart.NumberingDefinitionsPart;
if (numberingPart == null)
{
    numberingPart = document.MainDocumentPart.AddNewPart<NumberingDefinitionsPart>("NumberingDefinitionsPart001");
}
Share:
19,073
kjv
Author by

kjv

Updated on June 03, 2022

Comments

  • kjv
    kjv about 2 years

    Using the OpenXML SDK, 2.0 CTP, I am trying to programmatically create a Word document. In my document I have to insert a bulleted list, an some of the elements of the list must be underlined. How can I do this?

  • Dan
    Dan over 6 years
    Thank you so much for showing me how to add a list to a document. I was using Numbering.Append(abstactNum, numberingInstance) and could not understand why it does not work
  • Marius Conjeaud
    Marius Conjeaud over 6 years
    From the future : Thank you mate for that one ! Like, really ! :)
  • David Yates
    David Yates over 6 years
    @MariusConjeaud You're welcome. I created something for writing simple word documents that you are welcome to use (see github.com/madcodemonkey/SimpleDocument.OpenXML)
  • Marius Conjeaud
    Marius Conjeaud over 6 years
    Thanks, I'll check that out
  • H. de Jonge
    H. de Jonge about 6 years
    Great! Some improvements: The creation of the numbering instance could be done only once (separate method, returning the numberid). And the paragraph and run properties can also be added to the AbstractNum, so they don't have to be repeated for each item.
  • user1447679
    user1447679 over 4 years
    This has really helped me. I'm stuck on attempting multi level lists with this approach. Please see stackoverflow.com/questions/59093861/…
  • erkinyldz
    erkinyldz about 3 years
    Hello from 2021. I had all set up but it was not working. Thanks to this code I found out the problem was the ordering of abstractNum and Num elements. What a stupid rule.
  • james.garriss
    james.garriss about 3 years
    URL is now 404. :-(
  • AlbatrossCafe
    AlbatrossCafe almost 2 years
    This is hilarious. Using a the above symbol as a bullet makes it ridiculously small lol. I ended up using "•" instead and it looks better