Numbering and Lists
Overview
OOXML handles lists through a two-tier system: Abstract Numbering Definitions (templates) and Numbering Instances (actual lists).
Numbering Structure
Files Involved
/word/numbering.xml- List definitions/word/document.xml- Paragraphs reference lists
Two-Tier System
Abstract Numbering:
- Template defining list style
- Can have multiple levels (0-8)
- Reusable across multiple lists
Numbering Instance:
- References an abstract numbering
- Tracks current counter state
- Each independent list needs own instance
Abstract Numbering Definition
Structure
<w:abstractNum w:abstractNumId="0">
<w:multiLevelType w:val="hybridMultilevel"/>
<!-- Level 0 -->
<w:lvl w:ilvl="0">
<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:left="720" w:hanging="360"/>
</w:pPr>
</w:lvl>
<!-- Level 1 -->
<w:lvl w:ilvl="1">
<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:left="1440" w:hanging="360"/>
</w:pPr>
</w:lvl>
</w:abstractNum>
Number Formats
| Format | Result | XML Value |
|---|---|---|
| 1, 2, 3 | Decimal | decimal |
| a, b, c | Lower letter | lowerLetter |
| A, B, C | Upper letter | upperLetter |
| i, ii, iii | Lower Roman | lowerRoman |
| I, II, III | Upper Roman | upperRoman |
| • | Bullet | bullet |
Level Definition
Complete Level
<w:lvl w:ilvl="0">
<!-- Start value -->
<w:start w:val="1"/>
<!-- Number format -->
<w:numFmt w:val="decimal"/>
<!-- Level text (how number appears) -->
<w:lvlText w:val="%1."/>
<!-- Justification -->
<w:lvlJc w:val="left"/>
<!-- Paragraph properties -->
<w:pPr>
<w:ind w:left="720" w:hanging="360"/>
</w:pPr>
<!-- Run properties (number formatting) -->
<w:rPr>
<w:b/>
<w:color w:val="FF0000"/>
</w:rPr>
</w:lvl>
Level Text Placeholders
%1 = Level 1 number
%2 = Level 2 number
%3 = Level 3 number
Examples:
%1.→ "1.", "2.", "3."%1.%2→ "1.1", "1.2", "2.1"Article %1→ "Article 1", "Article 2"
Numbering Instance
Create Instance
<w:num w:numId="1">
<w:abstractNumId w:val="0"/>
</w:num>
Purpose: Links paragraphs to abstract numbering definition
Multiple instances: Each instance maintains independent counters
<w:num w:numId="1">
<w:abstractNumId w:val="0"/>
</w:num>
<w:num w:numId="2">
<w:abstractNumId w:val="0"/> <!-- Same template, different counter -->
</w:num>
Applying Lists to Paragraphs
In document.xml
<w:p>
<w:pPr>
<w:numPr>
<w:ilvl w:val="0"/> <!-- Level 0 -->
<w:numId w:val="1"/> <!-- List instance 1 -->
</w:numPr>
</w:pPr>
<w:r><w:t>First item</w:t></w:r>
</w:p>
<w:p>
<w:pPr>
<w:numPr>
<w:ilvl w:val="0"/>
<w:numId w:val="1"/>
</w:numPr>
</w:pPr>
<w:r><w:t>Second item</w:t></w:r>
</w:p>
<w:p>
<w:pPr>
<w:numPr>
<w:ilvl w:val="1"/> <!-- Level 1 (indented) -->
<w:numId w:val="1"/>
</w:numPr>
</w:pPr>
<w:r><w:t>Sub-item</w:t></w:r>
</w:p>
Result:
1. First item
2. Second item
a) Sub-item
Code Examples
Create Simple Numbered List
public static void CreateNumberedList(NumberingDefinitionsPart numberingPart)
{
if (numberingPart.Numbering == null)
{
numberingPart.Numbering = new Numbering();
}
// Abstract numbering
var abstractNum = new AbstractNum { AbstractNumberId = 0 };
abstractNum.AppendChild(new MultiLevelType { Val = MultiLevelValues.HybridMultilevel });
// Level 0: 1, 2, 3
var level0 = new Level { LevelIndex = 0 };
level0.AppendChild(new StartNumberingValue { Val = 1 });
level0.AppendChild(new NumberingFormat { Val = NumberFormatValues.Decimal });
level0.AppendChild(new LevelText { Val = "%1." });
level0.AppendChild(new LevelJustification { Val = LevelJustificationValues.Left });
var level0Props = new PreviousParagraphProperties();
level0Props.AppendChild(new Indentation { Left = "720", Hanging = "360" });
level0.AppendChild(level0Props);
abstractNum.AppendChild(level0);
numberingPart.Numbering.AppendChild(abstractNum);
// Numbering instance
var numberingInstance = new NumberingInstance { NumberID = 1 };
numberingInstance.AppendChild(new AbstractNumId { Val = 0 });
numberingPart.Numbering.AppendChild(numberingInstance);
}
Create Bullet List
public static void CreateBulletList(NumberingDefinitionsPart numberingPart)
{
if (numberingPart.Numbering == null)
{
numberingPart.Numbering = new Numbering();
}
// Abstract numbering
var abstractNum = new AbstractNum { AbstractNumberId = 1 };
abstractNum.AppendChild(new MultiLevelType { Val = MultiLevelValues.HybridMultilevel });
// Level 0: Bullet
var level0 = new Level { LevelIndex = 0 };
level0.AppendChild(new StartNumberingValue { Val = 1 });
level0.AppendChild(new NumberingFormat { Val = NumberFormatValues.Bullet });
level0.AppendChild(new LevelText { Val = "●" });
level0.AppendChild(new LevelJustification { Val = LevelJustificationValues.Left });
var level0Props = new PreviousParagraphProperties();
level0Props.AppendChild(new Indentation { Left = "720", Hanging = "360" });
level0.AppendChild(level0Props);
// Bullet font
var level0RunProps = new NumberingSymbolRunProperties();
level0RunProps.AppendChild(new RunFonts
{
Ascii = "Symbol",
HighAnsi = "Symbol",
Hint = FontTypeHintValues.Default
});
level0.AppendChild(level0RunProps);
abstractNum.AppendChild(level0);
numberingPart.Numbering.AppendChild(abstractNum);
// Numbering instance
var numberingInstance = new NumberingInstance { NumberID = 2 };
numberingInstance.AppendChild(new AbstractNumId { Val = 1 });
numberingPart.Numbering.AppendChild(numberingInstance);
}
Apply List to Paragraph
public static Paragraph CreateListItem(string text, int listId, int level = 0)
{
var paragraph = new Paragraph();
var paragraphProperties = new ParagraphProperties();
// Numbering properties
var numberingProperties = new NumberingProperties();
numberingProperties.AppendChild(new NumberingLevelReference { Val = level });
numberingProperties.AppendChild(new NumberingId { Val = listId });
paragraphProperties.AppendChild(numberingProperties);
paragraph.AppendChild(paragraphProperties);
var run = new Run();
run.AppendChild(new Text(text));
paragraph.AppendChild(run);
return paragraph;
}
// Usage:
body.AppendChild(CreateListItem("First item", 1, 0));
body.AppendChild(CreateListItem("Second item", 1, 0));
body.AppendChild(CreateListItem("Sub-item", 1, 1));
Multi-Level Lists
Three-Level List
public static void CreateMultiLevelList(NumberingDefinitionsPart numberingPart)
{
var abstractNum = new AbstractNum { AbstractNumberId = 2 };
abstractNum.AppendChild(new MultiLevelType { Val = MultiLevelValues.HybridMultilevel });
// Level 0: 1, 2, 3
var level0 = CreateLevel(0, NumberFormatValues.Decimal, "%1.", 720);
abstractNum.AppendChild(level0);
// Level 1: a, b, c
var level1 = CreateLevel(1, NumberFormatValues.LowerLetter, "%2)", 1440);
abstractNum.AppendChild(level1);
// Level 2: i, ii, iii
var level2 = CreateLevel(2, NumberFormatValues.LowerRoman, "%3.", 2160);
abstractNum.AppendChild(level2);
numberingPart.Numbering.AppendChild(abstractNum);
var numberingInstance = new NumberingInstance { NumberID = 3 };
numberingInstance.AppendChild(new AbstractNumId { Val = 2 });
numberingPart.Numbering.AppendChild(numberingInstance);
}
private static Level CreateLevel(
int levelIndex,
NumberFormatValues format,
string levelText,
int indentLeft)
{
var level = new Level { LevelIndex = levelIndex };
level.AppendChild(new StartNumberingValue { Val = 1 });
level.AppendChild(new NumberingFormat { Val = format });
level.AppendChild(new LevelText { Val = levelText });
level.AppendChild(new LevelJustification { Val = LevelJustificationValues.Left });
var props = new PreviousParagraphProperties();
props.AppendChild(new Indentation
{
Left = indentLeft.ToString(),
Hanging = "360"
});
level.AppendChild(props);
return level;
}
Common List Patterns
Restart Numbering
<w:p>
<w:pPr>
<w:numPr>
<w:ilvl w:val="0"/>
<w:numId w:val="1"/>
<w:ins w:id="0" w:author="User" w:date="..."/>
</w:numPr>
</w:pPr>
<w:r><w:t>Restarted at 1</w:t></w:r>
</w:p>
Use new numbering instance to restart:
// Create new instance with same abstract numbering
var newInstance = new NumberingInstance { NumberID = 4 };
newInstance.AppendChild(new AbstractNumId { Val = 0 }); // Same template
numberingPart.Numbering.AppendChild(newInstance);
Custom Start Number
<w:lvl w:ilvl="0">
<w:start w:val="5"/> <!-- Start at 5 instead of 1 -->
...
</w:lvl>
Skip Level
To skip numbering on a specific paragraph:
<w:p>
<!-- No w:numPr -->
<w:r><w:t>This paragraph has no number</w:t></w:r>
</w:p>
Custom Bullet Characters
Unicode Bullets
<w:lvlText w:val="●"/> <!-- Bullet -->
<w:lvlText w:val="○"/> <!-- Circle -->
<w:lvlText w:val="■"/> <!-- Square -->
<w:lvlText w:val="▪"/> <!-- Small square -->
<w:lvlText w:val="►"/> <!-- Arrow -->
<w:lvlText w:val="✓"/> <!-- Checkmark -->
Symbol Font Characters
var runProps = new NumberingSymbolRunProperties();
runProps.AppendChild(new RunFonts
{
Ascii = "Wingdings",
HighAnsi = "Wingdings"
});
level.AppendChild(runProps);
level.AppendChild(new LevelText { Val = "Ø" }); // Wingdings character
Indentation
Understanding Hanging Indent
|<------ left (720) ------>|
| |
1. Text starts here and
wraps to this position
|<- hanging (360) ->|
left: Total indent from margin
hanging: Distance number hangs out
Code
var indentation = new Indentation
{
Left = "720", // 0.5 inch from margin
Hanging = "360" // Number hangs 0.25 inch left of text
};
AppraiSys Usage
Extract from Codebase
AppraiSys uses lists minimally in templates, primarily for:
- Inspection checklists
- Comparable property adjustments
- Terms and conditions
Pattern:
- Define abstract numbering in
DefaultDocument.cs - Reference in templates
- Apply through mail merge or manual insertion
SFDT Lists
SFDT List Format
{
"listFormat": {
"listId": 0,
"listLevelNumber": 0
}
}
SFDT List Definition
{
"lists": [
{
"listId": 0,
"abstractListId": 0
}
],
"abstractLists": [
{
"abstractListId": 0,
"levels": [
{
"listLevelPattern": "Arabic",
"numberFormat": "%1.",
"startAt": 1,
"followCharacter": "Tab",
"paragraphFormat": {
"leftIndent": 36,
"firstLineIndent": -18
}
}
]
}
]
}
Common Mistakes
❌ Mistake 1: Forgetting Numbering Instance
<!-- WRONG: Only abstract numbering, no instance -->
<w:numbering>
<w:abstractNum w:abstractNumId="0">...</w:abstractNum>
<!-- Missing w:num -->
</w:numbering>
Fix: Always create both abstract and instance
❌ Mistake 2: Wrong Level Reference
<!-- WRONG: Referencing non-existent level -->
<w:numPr>
<w:ilvl w:val="5"/> <!-- But only levels 0-2 defined -->
<w:numId w:val="1"/>
</w:numPr>
Fix: Ensure level exists in abstract numbering
❌ Mistake 3: Reusing Number ID
// WRONG: Multiple instances with same numId
var num1 = new NumberingInstance { NumberID = 1 };
var num2 = new NumberingInstance { NumberID = 1 }; // Conflict!
Fix: Each instance needs unique NumberID
Debugging Lists
Check numbering.xml
- Extract DOCX
- Open
/word/numbering.xml - Verify abstract numbering exists
- Verify numbering instance exists
- Check level definitions
Validate References
// Get all list references in document
var listRefs = document.Descendants<NumberingId>()
.Select(n => n.Val.Value)
.Distinct();
// Check all exist in numbering definitions
foreach (var numId in listRefs)
{
var instance = numberingPart.Numbering.Descendants<NumberingInstance>()
.FirstOrDefault(n => n.NumberID == numId);
if (instance == null)
{
Console.WriteLine($"Missing numbering instance: {numId}");
}
}
Quick Reference
Minimum Valid List
<!-- numbering.xml -->
<w:numbering>
<w:abstractNum w:abstractNumId="0">
<w:lvl w:ilvl="0">
<w:start w:val="1"/>
<w:numFmt w:val="decimal"/>
<w:lvlText w:val="%1."/>
<w:lvlJc w:val="left"/>
</w:lvl>
</w:abstractNum>
<w:num w:numId="1">
<w:abstractNumId w:val="0"/>
</w:num>
</w:numbering>
<!-- document.xml -->
<w:p>
<w:pPr>
<w:numPr>
<w:ilvl w:val="0"/>
<w:numId w:val="1"/>
</w:numPr>
</w:pPr>
<w:r><w:t>List item</w:t></w:r>
</w:p>
Next Steps
➡️ Next: Styles In Depth
Related:
💡 Remember: Abstract numbering = template, Numbering instance = actual list counter!