Skip to content

Commit a3e638b

Browse files
ES-957510-MailMerge-XML-with-HTML-data
1 parent 410b6a3 commit a3e638b

File tree

6 files changed

+217
-0
lines changed

6 files changed

+217
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.12.35527.113 d17.12
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailMerge-XML-with-HTML-data", "MailMerge-XML-with-HTML-data\MailMerge-XML-with-HTML-data.csproj", "{5B0540C8-1A4D-4BB4-A0B9-10028D140E00}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{5B0540C8-1A4D-4BB4-A0B9-10028D140E00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{5B0540C8-1A4D-4BB4-A0B9-10028D140E00}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{5B0540C8-1A4D-4BB4-A0B9-10028D140E00}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{5B0540C8-1A4D-4BB4-A0B9-10028D140E00}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Root>
2+
<Validator>
3+
<Component>
4+
<Name>Login</Name>
5+
<Description>This component handles &lt;strong&gt; user authentication &lt;strong&gt;</Description>
6+
</Component>
7+
<Component>
8+
<Name>Dashboard</Name>
9+
<Description>The dashboard &lt;em&gt;summarizes&lt;em&gt; key metrics</Description>
10+
</Component>
11+
</Validator>
12+
</Root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<RootNamespace>MailMerge_XML_with_HTML_data</RootNamespace>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<Nullable>enable</Nullable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Syncfusion.DocIO.Net.Core" Version="*" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<None Update="Data\Data.xml">
17+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
18+
</None>
19+
<None Update="Data\Template.docx">
20+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
21+
</None>
22+
<None Update="Output\.gitkeep">
23+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
24+
</None>
25+
</ItemGroup>
26+
27+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
using Syncfusion.DocIO;
2+
using Syncfusion.DocIO.DLS;
3+
using System.Dynamic;
4+
using System.Xml;
5+
6+
7+
// Dictionary to temporarily hold merge field HTML values and their positions.
8+
Dictionary<WParagraph, Dictionary<int, string>> paraToInsertHTML = new Dictionary<WParagraph, Dictionary<int, string>>();
9+
10+
// Load the Word document template
11+
using (WordDocument document = new WordDocument(Path.GetFullPath(@"Data/Template.docx")))
12+
{
13+
// Load the XML file that contains data to be merged
14+
Stream xmlStream = System.IO.File.OpenRead(Path.GetFullPath(@"Data/Data.xml"));
15+
XmlDocument xmlDocument = new XmlDocument();
16+
xmlDocument.Load(xmlStream);
17+
xmlStream.Dispose(); // Close and release the file stream
18+
19+
// Convert the XML data into a dynamic ExpandoObject structure
20+
ExpandoObject allDataObject = new ExpandoObject();
21+
GetDataAsExpandoObject(xmlDocument.LastChild, ref allDataObject);
22+
23+
// Traverse the dynamic data to extract and map mail merge records
24+
if (allDataObject is IDictionary<string, object> allObjects &&
25+
allObjects.ContainsKey("Root"))
26+
{
27+
var rootObjects = (allObjects["Root"] as List<ExpandoObject>)[0] as IDictionary<string, object>;
28+
if (rootObjects.ContainsKey("Validator"))
29+
{
30+
var validatorObjects = (rootObjects["Validator"] as List<ExpandoObject>)[0] as IDictionary<string, object>;
31+
32+
// Iterate over each component inside Validator (e.g., Login, Dashboard)
33+
foreach (var validatorObject in validatorObjects)
34+
{
35+
var componentMainTag = validatorObject.Key; // Typically "Component"
36+
var componentData = validatorObject.Value as List<ExpandoObject>;
37+
38+
if (componentData != null)
39+
{
40+
// Create a mail merge data table for each component
41+
MailMergeDataTable componentDataTable = new MailMergeDataTable(componentMainTag, componentData);
42+
43+
// Attach event handler to handle HTML merge fields
44+
document.MailMerge.MergeField += new MergeFieldEventHandler(MergeFieldEvent);
45+
46+
// Execute group mail merge using nested data table
47+
document.MailMerge.ExecuteGroup(componentDataTable);
48+
}
49+
}
50+
}
51+
}
52+
53+
// After mail merge is complete, insert HTML into placeholders
54+
InsertHtml();
55+
56+
// Detach mail merge event handlers
57+
document.MailMerge.MergeField -= new MergeFieldEventHandler(MergeFieldEvent);
58+
59+
// Save the updated document
60+
document.Save(Path.GetFullPath(@"Output/Result.docx"), FormatType.Docx);
61+
}
62+
63+
/// <summary>
64+
/// Event handler that intercepts the mail merge process to handle HTML content.
65+
/// </summary>
66+
void MergeFieldEvent(object sender, MergeFieldEventArgs args)
67+
{
68+
if (args.FieldName.Equals("Description"))
69+
{
70+
// Get the paragraph containing the current merge field
71+
WParagraph paragraph = args.CurrentMergeField.OwnerParagraph;
72+
73+
// Get the index of the merge field inside the paragraph
74+
int mergeFieldIndex = paragraph.ChildEntities.IndexOf(args.CurrentMergeField);
75+
76+
// Store the HTML field value with its position
77+
Dictionary<int, string> fieldValues = new Dictionary<int, string>();
78+
fieldValues.Add(mergeFieldIndex, args.FieldValue.ToString());
79+
80+
// Associate the paragraph with its corresponding HTML values
81+
paraToInsertHTML.Add(paragraph, fieldValues);
82+
83+
// Remove the default text replacement; HTML will be inserted later
84+
args.Text = string.Empty;
85+
}
86+
}
87+
88+
/// <summary>
89+
/// Recursively parses XML nodes into ExpandoObject structure for easy access.
90+
/// Supports handling of HTML content inside text nodes.
91+
/// </summary>
92+
void GetDataAsExpandoObject(XmlNode node, ref ExpandoObject dynamicObject)
93+
{
94+
try
95+
{
96+
// Check if node is simple text (possibly with HTML tags)
97+
if (node.InnerText == node.InnerXml || (node.ChildNodes.Count == 1 && node.FirstChild.NodeType == XmlNodeType.Text))
98+
{
99+
if (!(dynamicObject as IDictionary<string, object>).ContainsKey(node.LocalName))
100+
(dynamicObject as IDictionary<string, object>).Add(node.LocalName, node.InnerText);
101+
}
102+
else
103+
{
104+
List<ExpandoObject> childObjects;
105+
106+
// Reuse existing list if it already exists for this node
107+
if ((dynamicObject as IDictionary<string, object>).ContainsKey(node.LocalName))
108+
childObjects = (dynamicObject as IDictionary<string, object>)[node.LocalName] as List<ExpandoObject>;
109+
else
110+
{
111+
childObjects = new List<ExpandoObject>();
112+
(dynamicObject as IDictionary<string, object>).Add(node.LocalName, childObjects);
113+
}
114+
115+
ExpandoObject childObject = new ExpandoObject();
116+
117+
// Recursively parse each child node
118+
foreach (XmlNode childNode in node.ChildNodes)
119+
{
120+
GetDataAsExpandoObject(childNode, ref childObject);
121+
}
122+
123+
childObjects.Add(childObject);
124+
}
125+
}
126+
catch (Exception e)
127+
{
128+
Console.WriteLine("Error in XML reading: " + e.ToString());
129+
}
130+
}
131+
132+
/// <summary>
133+
/// Replaces merge fields with actual HTML content using collected placeholders.
134+
/// </summary>
135+
void InsertHtml()
136+
{
137+
// Iterate each paragraph and its associated HTML values
138+
foreach (KeyValuePair<WParagraph, Dictionary<int, string>> dictionaryItems in paraToInsertHTML)
139+
{
140+
WParagraph paragraph = dictionaryItems.Key;
141+
Dictionary<int, string> values = dictionaryItems.Value;
142+
143+
foreach (KeyValuePair<int, string> valuePair in values)
144+
{
145+
int index = valuePair.Key;
146+
string fieldValue = valuePair.Value;
147+
148+
// Insert HTML at the specified location in the paragraph
149+
paragraph.OwnerTextBody.InsertXHTML(fieldValue, paragraph.OwnerTextBody.ChildEntities.IndexOf(paragraph), index);
150+
}
151+
}
152+
153+
// Clear dictionary after inserting all HTML
154+
paraToInsertHTML.Clear();
155+
}

0 commit comments

Comments
 (0)