Skip to main content

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 delete
  • sdtLocked - Can edit content but not delete SDT
  • contentLocked - Cannot edit content but can delete SDT
  • sdtContentLocked - 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:


💡 Remember: SDTs are containers, not content. They hold paragraphs (block) or runs (inline), never the other way around!