Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a multi-level bullet list with Word.Interop

I need to create a multi-level bullet list via Microsoft.Office.Interop.Word and I am currently struggling with its (horrible) API (again).

I've just created the following example (not dynamic yet, just for demonstration purposes) in a VSTO document-level project for Microsoft Office Word 2010 in the programming language C#:

Word.Paragraph paragraph = null;
Word.Range range = this.Content;
paragraph = range.Paragraphs.Add();
paragraph.Range.Text = "Item 1";
paragraph.Range.ListFormat.ApplyBulletDefault(Word.WdDefaultListBehavior.wdWord10ListBehavior);
// ATTENTION: We have to outdent the paragraph AFTER its list format has been set, otherwise this has no effect.
// Without this, the the indent of "Item 2" differs from the indent of "Item 1".
paragraph.Outdent();

paragraph.Range.InsertParagraphAfter();

paragraph = range.Paragraphs.Add();
paragraph.Range.Text = "Item 1.1";
// ATTENTION: We have to indent the paragraph AFTER its text has been set, otherwise this has no effect.
paragraph.Indent();
paragraph.Range.InsertParagraphAfter();

paragraph = range.Paragraphs.Add();
paragraph.Range.Text = "Item 1.2";
paragraph.Range.InsertParagraphAfter();

paragraph = range.Paragraphs.Add();
paragraph.Range.Text = "Item 2";
paragraph.Outdent();

The code does exactly what I want (after a lot of try and error!), but it's horrible in my opinion. The format has to be applied at a VERY specific point and I have to manually indent and outdent the created paragraphs.

So my question is: Does a better approach exist to create a multi-level bullet list via Word.Interop, e.g. via shorthand methods that I haven't discovered yet?

My goal is to create a multi-level list from XML data (more specific a CustomXMLNode object)

Two other questions related to bullet lists exist on Stack Overflow, but both do not help me (the source code above is one answer to the second question):

  • Bullet points in Word with c# Interop
  • https://stackoverflow.com/questions/3768414/ms-word-list-with-sub-lists

EDIT (2013-08-08):

I've just hacked something together that outputs two arrays as a bullet list with two levels (the array with the sub-items is used for each root-item, to keep it simple). By introducing recursion, one would be able to create a bullet list with infinite levels (theoretically). But the problem remains, the code is a mess...

string[] rootItems = new string[]
{
    "Root Item A", "Root Item B", "Root Item C"
};

string[] subItems = new string[]
{
    "Subitem A", "Subitem B"
};

Word.Paragraph paragraph = null;
Word.Range range = this.Content;
bool appliedListFormat = false;
bool indented = false;

for (int i = 0; i < rootItems.Length; ++i)
{
    paragraph = range.Paragraphs.Add();
    paragraph.Range.Text = rootItems[i];

    if (!appliedListFormat)
    {
        paragraph.Range.ListFormat.ApplyBulletDefault(Word.WdDefaultListBehavior.wdWord10ListBehavior);
        appliedListFormat = true;
    }

    paragraph.Outdent();
    paragraph.Range.InsertParagraphAfter();

    for (int j = 0; j < subItems.Length; ++j)
    {
        paragraph = range.Paragraphs.Add();
        paragraph.Range.Text = subItems[j];

        if (!indented)
        {
            paragraph.Indent();
            indented = true;
        }

        paragraph.Range.InsertParagraphAfter();
    }

    indented = false;
}

// Delete the last paragraph, since otherwise the list ends with an empty sub-item.
paragraph.Range.Delete();

EDIT (2013-08-12):

Last friday I thought I have achieved what I wanted to, but this morning I noticed, that my solution only works if the insertion point is at the end of the document. I've created the following simple example to demonstrate the (erroneous) behavior. To conclude my problem: I am able to create multi-level bullet lists at the end of the document only. As soon as I change the current selection (e.g. to the start of the document), the list is destroyed. As far as I can see, this is related to the (automatic or non-automatic) extension of the Range objects. I've tried a lot so far (I'm almost losing it), but it's all cargo-cult to me. The only thing I want to do is to insert one element after another (is it impossible to create a content control inside a paragraph, so that the text of the paragraph is followed by the content control?) and to to that in any Range of a Document. I will create a Gist on GitHub with my actual CustomXMLPart binding class this evening. Eventually someone can help me to fix that bothersome problem.

private void buttonTestStatic_Click(object sender, RibbonControlEventArgs e)
{
    Word.Range range = Globals.ThisDocument.Application.Selection.Range;
    Word.ListGallery listGallery = Globals.ThisDocument.Application.ListGalleries[Word.WdListGalleryType.wdBulletGallery];
    Word.Paragraph paragraph = null;
    Word.ListFormat listFormat = null;

    // TODO At the end of the document, the ranges are automatically expanded and inbetween not?

    paragraph = range.Paragraphs.Add();
    listFormat = paragraph.Range.ListFormat;
    paragraph.Range.Text = "Root Item A";
    this.ApplyListTemplate(listGallery, listFormat, 1);
    paragraph.Range.InsertParagraphAfter();

    paragraph = paragraph.Range.Paragraphs.Add();
    listFormat = paragraph.Range.ListFormat;
    paragraph.Range.Text = "Child Item A.1";
    this.ApplyListTemplate(listGallery, listFormat, 2);
    paragraph.Range.InsertParagraphAfter();

    paragraph = paragraph.Range.Paragraphs.Add();
    listFormat = paragraph.Range.ListFormat;
    paragraph.Range.Text = "Child Item A.2";
    this.ApplyListTemplate(listGallery, listFormat, 2);
    paragraph.Range.InsertParagraphAfter();

    paragraph = paragraph.Range.Paragraphs.Add();
    listFormat = paragraph.Range.ListFormat;
    paragraph.Range.Text = "Root Item B";
    this.ApplyListTemplate(listGallery, listFormat, 1);
    paragraph.Range.InsertParagraphAfter();
}

private void ApplyListTemplate(Word.ListGallery listGallery, Word.ListFormat listFormat, int level = 1)
{
    listFormat.ApplyListTemplateWithLevel(
        listGallery.ListTemplates[level],
        ContinuePreviousList: true,
        ApplyTo: Word.WdListApplyTo.wdListApplyToSelection,
        DefaultListBehavior: Word.WdDefaultListBehavior.wdWord10ListBehavior,
        ApplyLevel: level);
}

EDIT (2013-08-12): I've set up a GitHub repository here which demonstrates my problem with the Word.Range objects. The OnClickButton method in the file Ribbon.cs invokes my custom mapper class. The comments there describe the problem. I know that my problems are related to the argument Word.Range object reference, but all other solutions I tried (e.g. modifying the range inside of the class) failed even harder. The best solution I've achieved so far, is to specify the Document.Content range as the argument for the MapToCustomControlsIn method. This inserts a nicely formatted multi-level bullet list (with custom XML parts bound to content controls) to the end of the document. What I want is to insert that list at a custom posiztion into the document (e.g. the current selection via Word.Selection.Range).

like image 272
Florian Wolters Avatar asked Nov 13 '22 00:11

Florian Wolters


1 Answers

Florian Wolters example almost there, but the first child item numbering always not correct when I tried.

Someone gave me inspiration by suggesting using Macro and VBA script then convert to C#.

Below is the sample code tested works at my side. Hope it helps.

using Microsoft.Office.Interop.Word;
using System.Reflection;

namespace OfficeUtility
{
    public class NumberListGenerate
    {
        public void GenerateList()
        {
            Application app = null;
            Document doc = null;
            string filePath = "c:\\output.docx";
            string pdfPath = "c:\\export.pdf";

            try
            {
                app = new Application();
                app.Visible = false;    // Open Microsoft Office in background
                doc = app.Documents.Open(filePath, Missing.Value, false);

                Range range = doc.Range();
                string search = "$list";
 
                // Find in document to generate list
                while (range.Find.Execute(search))
                {
                    ListGallery listGallery = 
                        app.ListGalleries[WdListGalleryType.wdNumberGallery];

                    // Select found location
                    range.Select();

                    // Apply multi level list
                    app.Selection.Range.ListFormat.ApplyListTemplateWithLevel(
                        listGallery.ListTemplates[1],
                        ContinuePreviousList: false,
                        ApplyTo: WdListApplyTo.wdListApplyToWholeList, 
                        DefaultListBehavior: WdDefaultListBehavior.wdWord10ListBehavior);

                    // First level
                    app.Selection.TypeText("Root Item A");  // Set text to key in
                    app.Selection.TypeParagraph();  // Simulate typing in MS Word

                    // Go to 2nd level
                    app.Selection.Range.ListFormat.ListIndent();
                    app.Selection.TypeText("Child Item A.1");
                    app.Selection.TypeParagraph();
                    app.Selection.TypeText("Child Item A.2");
                    app.Selection.TypeParagraph();

                    // Back to 1st level
                    app.Selection.Range.ListFormat.ListOutdent(); 
                    app.Selection.TypeText("Root Item B");
                    app.Selection.TypeParagraph();

                    // Go to 2nd level
                    app.Selection.Range.ListFormat.ListIndent();
                    app.Selection.TypeText("Child Item B.1");
                    app.Selection.TypeParagraph();
                    app.Selection.TypeText("Child Item B.2");
                    app.Selection.TypeParagraph();

                    // Delete empty item generated by app.Selection.TypeParagraph();
                    app.Selection.TypeBackspace();
                }

                // Save document
                doc.Save();

                // Export to pdf 
                doc.ExportAsFixedFormat(pdfPath, WdExportFormat.wdExportFormatPDF);                   
            }
            catch (System.Exception ex)
            {
                LogError(ex);
            }
            finally
            {
                if (doc != null)
                {
                    // Need to close the document to prevent deadlock
                    doc.Close(false);
                    System.Runtime.InteropServices.Marshal.FinalReleaseComObject(doc);
                }

                if (app != null)
                {
                    app.Quit();
                    System.Runtime.InteropServices.Marshal.FinalReleaseComObject(app);
                }
            }
        }
    }
}
like image 139
Tong Yin Wang Avatar answered Nov 14 '22 23:11

Tong Yin Wang