Content Controls (SDT)
What Are Content Controls?
Content controls (Structured Document Tags or SDT) are containers in Word documents that can hold content and have special properties. They're commonly used for:
- Mail merge fields - Placeholders for dynamic content
- Form fields - Input areas in forms
- Repeating sections - Dynamic lists
- Drop-down lists - Selection options
- Date pickers - Date selection
- Rich text areas - Formatted content input
In AppraiSys, SDTs are heavily used for mail merge in appraisal reports.
SDT Structure
Complete SDT Anatomy
<w:sdt>
<!-- 1. Properties (required) -->
<w:sdtPr>
<w:id w:val="123456"/> <!-- Unique ID -->
<w:tag w:val="MergeField1"/> <!-- Tag for identification -->
<w:alias w:val="Customer Name"/> <!-- Display name -->
<w:lock w:val="sdtContentLocked"/> <!-- Lock behavior -->
<w:placeholder> <!-- Placeholder text -->
<w:docPart w:val="DefaultPlaceholder"/>
</w:placeholder>
<w:text/> <!-- Plain text control (optional) -->
</w:sdtPr>
<!-- 2. End Properties (optional) -->
<w:sdtEndPr/>
<!-- 3. Content (required) -->
<w:sdtContent>
<w:p>
<w:r>
<w:t>Content goes here</w:t>
</w:r>
</w:p>
</w:sdtContent>
</w:sdt>
Block-Level vs Inline SDTs
Block-Level SDT
Location: Direct child of body, table cell, or another block container
Can contain: Paragraphs, tables, other block elements
Use case: Multi-paragraph content, tables, sections
<!-- ✅ Block SDT in body -->
<w:body>
<w:sdt>
<w:sdtPr>
<w:tag w:val="PropertyDescription"/>
</w:sdtPr>
<w:sdtContent>
<w:p>
<w:r><w:t>First paragraph</w:t></w:r>
</w:p>
<w:p>
<w:r><w:t>Second paragraph</w:t></w:r>
</w:p>
<w:tbl>
<!-- Table content -->
</w:tbl>
</w:sdtContent>
</w:sdt>
</w:body>
Inline SDT
Location: Inside a paragraph
Can contain: Only runs (inline content)
Use case: Single values, inline fields, short text
<!-- ✅ Inline SDT in paragraph -->
<w:p>
<w:r><w:t>The property value is </w:t></w:r>
<w:sdt>
<w:sdtPr>
<w:tag w:val="AppraisedValue"/>
</w:sdtPr>
<w:sdtContent>
<w:r>
<w:t>$450,000</w:t>
</w:r>
</w:sdtContent>
</w:sdt>
<w:r><w:t>.</w:t></w:r>
</w:p>
SDT Types
1. Plain Text Control
Purpose: Single-line or multi-line plain text
Properties:
<w:sdtPr>
<w:text w:multiLine="true"/> <!-- Allow multiple lines -->
<w:tag w:val="TextField"/>
</w:sdtPr>
Use in AppraiSys: Property addresses, description fields
2. Rich Text Control
Purpose: Formatted text with styles
Properties:
<w:sdtPr>
<w:richText/>
<w:tag w:val="NotesField"/>
</w:sdtPr>
Use in AppraiSys: Appraiser notes, comments
3. Drop-down List
Purpose: Selection from predefined options
Properties:
<w:sdtPr>
<w:dropDownList>
<w:listItem w:displayText="Single Family" w:value="SF"/>
<w:listItem w:displayText="Condo" w:value="CONDO"/>
<w:listItem w:displayText="Multi-Family" w:value="MF"/>
</w:dropDownList>
<w:tag w:val="PropertyType"/>
</w:sdtPr>
Use in AppraiSys: Property type selection, condition ratings
4. Date Picker
Purpose: Date selection with calendar
Properties:
<w:sdtPr>
<w:date>
<w:dateFormat w:val="MM/dd/yyyy"/>
<w:calendar w:val="gregorian"/>
</w:date>
<w:tag w:val="InspectionDate"/>
</w:sdtPr>
Use in AppraiSys: Inspection dates, sale dates
5. Picture Content Control
Purpose: Placeholder for images
Properties:
<w:sdtPr>
<w:picture/>
<w:tag w:val="PropertyPhoto"/>
</w:sdtPr>
Use in AppraiSys: Property photos, comparable photos
6. Repeating Section
Purpose: Dynamic lists that repeat
Properties:
<w:sdtPr>
<w:repeatingSectionItem/>
<w:tag w:val="ComparableItem"/>
</w:sdtPr>
Use in AppraiSys: Comparable properties list
7. Checkbox
Purpose: Boolean selection
Properties:
<w:sdtPr>
<w14:checkbox>
<w14:checked w14:val="1"/> <!-- 1 = checked, 0 = unchecked -->
<w14:checkedState w14:val="2612" w14:font="Segoe UI Symbol"/>
</w14:checkbox>
<w:tag w:val="HasGarage"/>
</w:sdtPr>
SDT Properties Explained
ID (w:id)
Purpose: Unique identifier for the content control
Value: Integer (automatically generated)
<w:id w:val="123456"/>
Important: Must be unique within the document
Tag (w:tag)
Purpose: Machine-readable identifier for mail merge
Value: String (your field name)
<w:tag w:val="PropertyAddress"/>
In AppraiSys: This maps to your merge field names in the template
Alias (w:alias)
Purpose: Human-readable display name
Value: String (displayed to users)
<w:alias w:val="Property Street Address"/>
Use: Shows up in Word's Developer tab
Lock Settings (w:lock)
Purpose: Control editing behavior
Values:
unlocked- Can edit and deletesdtLocked- Can edit content but not delete SDTcontentLocked- Cannot edit content but can delete SDTsdtContentLocked- Cannot edit or delete
<w:lock w:val="sdtContentLocked"/>
Common in AppraiSys: Lock SDTs after mail merge to prevent accidental changes
SDTs in Mail Merge
AppraiSys Pattern
From SyncfusionFieldsPipelineStep.cs:
// Document has SDTs with tags like "PropertyAddress", "AppraisedValue"
DestinationDocument = new WordDocument(input.DocumentData, FormatType.Docx);
// Mail merge clears and fills SDTs
DestinationDocument.MailMerge.ClearFields = false;
DestinationDocument.MailMerge.BeforeClearField += BeforeClearFieldWrapper;
// Execute merge
DestinationDocument.MailMerge.Execute(new List<string> { "" });
Creating SDT for Mail Merge
// Create block SDT with tag
var sdt = new SdtBlock();
// Properties
var sdtProperties = new SdtProperties(
new Tag { Val = "PropertyAddress" }, // Match merge field
new Alias { Val = "Property Address" }
);
sdt.Append(sdtProperties);
// Content with placeholder
var sdtContent = new SdtContentBlock();
var paragraph = new Paragraph();
var run = new Run();
run.Append(new RunProperties(new Color { Val = "0B5394" })); // Blue placeholder
run.Append(new Text("[Property Address]"));
paragraph.Append(run);
sdtContent.Append(paragraph);
sdt.Append(sdtContent);
body.Append(sdt);
Merge Field Mapping
<!-- Template SDT -->
<w:sdt>
<w:sdtPr>
<w:tag w:val="AppraisedValue"/>
</w:sdtPr>
<w:sdtContent>
<w:p>
<w:r>
<w:rPr><w:color w:val="0B5394"/></w:rPr>
<w:t>[Value will be inserted]</w:t>
</w:r>
</w:p>
</w:sdtContent>
</w:sdt>
<!-- After mail merge -->
<w:sdt>
<w:sdtPr>
<w:tag w:val="AppraisedValue"/>
</w:sdtPr>
<w:sdtContent>
<w:p>
<w:r>
<w:t>$450,000</w:t>
</w:r>
</w:p>
</w:sdtContent>
</w:sdt>
Common SDT Mistakes
❌ Mistake 1: SDT Inside Run
<!-- WRONG -->
<w:p>
<w:r>
<w:sdt>...</w:sdt>
</w:r>
</w:p>
Fix: Place SDT inside paragraph (for inline) or at body level (for block)
<!-- CORRECT Inline -->
<w:p>
<w:sdt>
<w:sdtContent>
<w:r>...</w:r>
</w:sdtContent>
</w:sdt>
</w:p>
<!-- CORRECT Block -->
<w:body>
<w:sdt>
<w:sdtContent>
<w:p>...</w:p>
</w:sdtContent>
</w:sdt>
</w:body>
❌ Mistake 2: Empty SDT Content
<!-- PROBLEMATIC -->
<w:sdt>
<w:sdtPr>...</w:sdtPr>
<w:sdtContent/> <!-- Empty! -->
</w:sdt>
Fix: Always have at least one paragraph
<!-- CORRECT -->
<w:sdt>
<w:sdtPr>...</w:sdtPr>
<w:sdtContent>
<w:p/> <!-- Empty paragraph OK -->
</w:sdtContent>
</w:sdt>
❌ Mistake 3: Duplicate IDs
<!-- WRONG: Same ID used twice -->
<w:sdt>
<w:sdtPr>
<w:id w:val="123"/>
</w:sdtPr>
...
</w:sdt>
<w:sdt>
<w:sdtPr>
<w:id w:val="123"/> <!-- Duplicate! -->
</w:sdtPr>
...
</w:sdt>
Fix: Use unique IDs (SDK generates them automatically)
Working with SDTs Programmatically
Finding SDTs by Tag
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
public SdtBlock FindSdtByTag(WordprocessingDocument doc, string tag)
{
var sdts = doc.MainDocumentPart.Document.Body
.Descendants<SdtBlock>()
.Where(sdt => sdt.SdtProperties
.GetFirstChild<Tag>()?.Val == tag);
return sdts.FirstOrDefault();
}
// Usage
var addressSdt = FindSdtByTag(doc, "PropertyAddress");
if (addressSdt != null)
{
// Update content
var content = addressSdt.SdtContentBlock;
content.RemoveAllChildren<Paragraph>();
content.Append(new Paragraph(
new Run(new Text("123 Main Street"))
));
}
Creating Inline SDT
public SdtRun CreateInlineSdt(string tag, string text)
{
var sdt = new SdtRun();
// Properties
sdt.SdtProperties = new SdtProperties(
new Tag { Val = tag },
new SdtId { Val = GenerateUniqueId() }
);
// Content
var content = new SdtContentRun();
content.Append(new Run(new Text(text)));
sdt.Append(content);
return sdt;
}
// Usage in paragraph
paragraph.Append(new Run(new Text("Value: ")));
paragraph.Append(CreateInlineSdt("Amount", "$450,000"));
Creating Block SDT
public SdtBlock CreateBlockSdt(string tag, params Paragraph[] paragraphs)
{
var sdt = new SdtBlock();
// Properties
sdt.SdtProperties = new SdtProperties(
new Tag { Val = tag },
new SdtId { Val = GenerateUniqueId() },
new Lock { Val = LockingValues.SdtContentLocked }
);
// Content
var content = new SdtContentBlock();
foreach (var para in paragraphs)
{
content.Append(para);
}
sdt.Append(content);
return sdt;
}
// Usage
var sdt = CreateBlockSdt(
"PropertyDescription",
new Paragraph(new Run(new Text("Line 1"))),
new Paragraph(new Run(new Text("Line 2")))
);
body.Append(sdt);
Nested SDTs
When Valid
SDTs can be nested if they're at different levels:
<!-- ✅ VALID: Block SDT contains paragraph with inline SDT -->
<w:sdt> <!-- Block SDT -->
<w:sdtPr><w:tag w:val="Section"/></w:sdtPr>
<w:sdtContent>
<w:p>
<w:r><w:t>Name: </w:t></w:r>
<w:sdt> <!-- Inline SDT -->
<w:sdtPr><w:tag w:val="Name"/></w:sdtPr>
<w:sdtContent>
<w:r><w:t>John</w:t></w:r>
</w:sdtContent>
</w:sdt>
</w:p>
</w:sdtContent>
</w:sdt>
When Invalid
<!-- ❌ WRONG: Run inside SDT inside run -->
<w:p>
<w:r>
<w:sdt>...</w:sdt> <!-- Invalid! -->
</w:r>
</w:p>
SDT Persistence After Merge
Problem
After mail merge, SDTs may be removed or "flattened":
<!-- Before merge -->
<w:sdt>
<w:sdtPr><w:tag w:val="Field"/></w:sdtPr>
<w:sdtContent>
<w:p><w:r><w:t>[Placeholder]</w:t></w:r></w:p>
</w:sdtContent>
</w:sdt>
<!-- After merge (SDT removed!) -->
<w:p>
<w:r><w:t>Actual Value</w:t></w:r>
</w:p>
Solution in AppraiSys
// Set ClearFields = false to keep SDT structure
DestinationDocument.MailMerge.ClearFields = false;
Repeating Sections
Structure
<w:sdt>
<w:sdtPr>
<w:repeatingSectionItem/>
</w:sdtPr>
<w:sdtContent>
<!-- Template for each item -->
<w:p>
<w:r><w:t>Item content</w:t></w:r>
</w:p>
</w:sdtContent>
</w:sdt>
In AppraiSys
Used for comparable properties:
// Create repeating section for comparables
var repeatingSdt = new SdtBlock();
repeatingSdt.SdtProperties = new SdtProperties(
new Tag { Val = "ComparablesList" },
new SdtRepeatingSection()
);
// Template for each comparable
var itemTemplate = new SdtBlock();
itemTemplate.SdtProperties = new SdtProperties(
new SdtRepeatingItem()
);
var itemContent = new SdtContentBlock();
itemContent.Append(CreateComparableTemplate());
itemTemplate.Append(itemContent);
repeatingSdt.SdtContentBlock.Append(itemTemplate);
Visual Summary
Quick Reference
Block SDT Template
<w:sdt>
<w:sdtPr>
<w:id w:val="[unique-id]"/>
<w:tag w:val="[field-name]"/>
</w:sdtPr>
<w:sdtContent>
<w:p>
<w:r><w:t>[content]</w:t></w:r>
</w:p>
</w:sdtContent>
</w:sdt>
Inline SDT Template
<w:p>
<w:sdt>
<w:sdtPr>
<w:id w:val="[unique-id]"/>
<w:tag w:val="[field-name]"/>
</w:sdtPr>
<w:sdtContent>
<w:r><w:t>[content]</w:t></w:r>
</w:sdtContent>
</w:sdt>
</w:p>
Next Steps
➡️ Next: Tables in OOXML - Complete table guide
Related:
- Common Fatal Mistakes - SDT placement errors
- Element Hierarchy Rules - Valid SDT locations
- Code Examples - Working code
💡 Remember: SDTs are containers, not content. They hold paragraphs (block) or runs (inline), never the other way around!