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):
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
).
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);
}
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With