Wrapping entire FieldToa content into StructuredDocumentTag(content control)

Hi, I am trying to wrap entire content in FieldTOA to StructuredDocumentTag using below code but it looks like it is not wrapping it. Could you please help me to fix this issue?

FieldToa fieldToa = null;
fieldToa = (FieldToa)(sdt.ParentNode as Paragraph).InsertField(FieldType.FieldTOA, false, sdt, false);

Regards,
Chetan

@KCSR it is possible wrap TOA inside SDT, to achieve this the SDT must be of type SdtType.RichText and the markup level must be set to MarkupLevel.Block, please use the following code as reference:

Document doc = new Document();
DocumentBuilder builder = new DocumentBuilder(doc);

StructuredDocumentTag pref = new StructuredDocumentTag(doc, SdtType.RichText, MarkupLevel.Block);
doc.FirstSection.Body.AppendChild(pref);
builder.MoveToStructuredDocumentTag(pref, 0);

FieldToa fieldToa = (FieldToa)builder.InsertField(FieldType.FieldTOA, false);
fieldToa.EntryCategory = "1";
fieldToa.UseHeading = true;
fieldToa.BookmarkName = "MyBookmark";
fieldToa.EntrySeparator = " \t p.";
fieldToa.PageNumberListSeparator = " & p. ";
fieldToa.UsePassim = true;
fieldToa.PageRangeSeparator = " to ";
fieldToa.RemoveEntryFormatting = true;


builder.InsertBreak(BreakType.PageBreak);
FieldTA fieldTA = InsertToaEntry(builder, "1", "Source 1");
builder.StartBookmark("MyBookmark");
fieldTA = InsertToaEntry(builder, "2", "Source 2");
fieldTA = InsertToaEntry(builder, "1", "Source 3");
fieldTA.ShortCitation = "S.3";
fieldTA = InsertToaEntry(builder, "1", "Source 2");
fieldTA.IsBold = false;
fieldTA.IsItalic = true;
fieldTA = InsertToaEntry(builder, "1", "Source 3");
fieldTA.PageRangeBookmarkName = "MyMultiPageBookmark";

builder.StartBookmark("MyMultiPageBookmark");
builder.InsertBreak(BreakType.PageBreak);
builder.InsertBreak(BreakType.PageBreak);
builder.InsertBreak(BreakType.PageBreak);
builder.EndBookmark("MyMultiPageBookmark");

for (int i = 0; i < 5; i++)
{
    InsertToaEntry(builder, "1", "Source 4");
}

builder.EndBookmark("MyBookmark");

doc.UpdateFields();
doc.Save(@"C:\Temp\Field.TOA.TA.docx");
1 Like

Hi Eduardo, Thanks for the reply!

In my case the SDT will be provided by other source and this SDT type is RichText and it contains ID and Title. In my code, I will be finding the SDT in the document and insert TOA in that location. Below is the code that i am using to find the SDT location which is working as expected.

StructuredDocumentTag sdt = doc?.GetChildNodes(NodeType.StructuredDocumentTag, true).OfType<StructuredDocumentTag>().FirstOrDefault(x => x.Id == Convert.ToInt64(InsertToaLocation));

Where ‘InsertToaLocation’ is the Id value of the SDT provided by other source.

You mentioned that SDT’s markup should be set to MarkupLevel.Block, is it possible to set that after finding the SDT in above code OR should i ask them to do this setting before sharing the SDT to me?

I did go through the code that you have shared, it looks like we are moving the builder to SDT start and inserting the FieldToa. I don’t see builder.MoveToStructuredDocumentTag(pref, 0); instead I see only builder.MoveTo(pref) so i think in my case it is going to SDT location and inserting TOA after that SDT location instead of wrapping the TOA content inside that SDT.

Could you please help me on the above?
In simple words, I need to wrap the entire TOA Content inside the SDT which already exist in the document. TOA Content will be constructed by my code which is already implemented.

REgards,
Chetan

@KCSR Unfortunately, there is no way change markup level on SDT, you can only remove old SDT and insert another with different markup level.

In the code you posted in your initial post, the TOA field is inserted after the SDT, since you are using Paragraph.InsertField method.

In your case you should use DocumentBuilder and move it inside the SDT:

Document doc = new Document(@"C:\Temp\in.docx");
DocumentBuilder builder= new DocumentBuilder(doc);

// Get SDT. For demonstraction purposes get the first one.
StructuredDocumentTag targetSdt = (StructuredDocumentTag)doc.GetChild(NodeType.StructuredDocumentTag, 0, true);

// Move buildr inside SDT.
builder.MoveToStructuredDocumentTag(targetSdt, 0);

// Insert TOA field
builder.InsertField(FieldType.FieldTOA, false);

doc.Save(@"C:\Temp\out.docx");
2 Likes

Thanks Alexey for the reply.

For some reason I didn’t see the below.
builder.MoveToStructuredDocumentTag() instead I see only builder.MoveTo(Node) …

Is this related to Aspose version? In which version we have builder.MoveToStructuredDocumentTag() ?

Regards,
Chetan

@KCSR This method has been introduced in 22.10 version of Aspose.Words:
https://docs.aspose.com/words/net/aspose-words-for-net-22-10-release-notes/#implemented-ability-to-move-documentbuilder-cursor-inside-structured-document-tag

1 Like

Thanks Alexey, I tried upgrading my Aspose version to latest, but I need to get the license yet. Just to make sure things are working expected. I tested the below method

// Move buildr inside SDT.
builder.MoveToStructuredDocumentTag(targetSdt, 0);

I see only the below content is wrapped inside the SDT. Is this because we have not set SDT to MarkupLevel.Block?
TOA \h \f \c 8 \e " "

In your earlier comments you suggested to remove old SDT and insert another with different markup level. I tried below but I still see the SDT exist. Could you please help me how we achieve this? I want to insert the new SDT in the same place of old SDT. I can find the old SDT as shown in below code.

StructuredDocumentTag sdt = doc?.GetChildNodes(NodeType.StructuredDocumentTag, true).OfType<StructuredDocumentTag>().FirstOrDefault(x => x.Id == Convert.ToInt64(InsertToaLocation));

builder.MoveToStructuredDocumentTag(sdt, 0);
sdt.Remove();

Regards,
Chetan

@KCSR You can use code like this to check whether SDT is inline and insert block level SDT instead it:

Document doc = new Document(@"C:\Temp\in.docx");
DocumentBuilder builder = new DocumentBuilder(doc);

List<StructuredDocumentTag> sdts = doc.GetChildNodes(NodeType.StructuredDocumentTag, true).Cast<StructuredDocumentTag>().ToList();

foreach (StructuredDocumentTag sdt in sdts)
{
    StructuredDocumentTag currentSDT = sdt;
    // Replace inline SDT with block level.
    if (currentSDT.Level == MarkupLevel.Inline)
    {
        builder.MoveTo(currentSDT);
        builder.Writeln();
        StructuredDocumentTag newSdt = new StructuredDocumentTag(doc, SdtType.RichText, MarkupLevel.Block);
        builder.CurrentParagraph.ParentNode.InsertBefore(newSdt, builder.CurrentParagraph);
        // Remove Old SDT
        currentSDT.Remove();
        currentSDT = newSdt;
    }

    // Remove old content from SDT.
    currentSDT.RemoveAllChildren();
    // Ensure there are nodes in SDT where DocumentBuilder can be moved.
    currentSDT.AppendChild(new Paragraph(doc));

    builder.MoveToStructuredDocumentTag(currentSDT, 0);
    builder.InsertField("TOA \\h \\c \"1\" \\p");
}

// Update fields.
doc.UpdateFields();

doc.Save(@"C:\Temp\out.docx");
1 Like

Thanks alot Alexey, that helped!

I am able to get the TOA content wrapped in the SDT now.

TOA structure also looks good as shown in below screenshot. Could you please let me know why my page numbers are not created as jumplinks and it is not jumping to referenced text(bookmark) in that page?

My TOA Entry code is as shown below -

FieldTA field = (FieldTA)builder.InsertField(FieldType.FieldTOAEntry, false);
field.EntryCategory = Convert.ToString(entryCategory);
if (model.IsPartyNameOnSeparateLines && !string.IsNullOrEmpty(model.FullPartyName))
{
    field.LongCitation = model.FullPartyNameWithBreak;
}
else
{
   field.LongCitation = longCitation;
}
field.PageRangeBookmarkName = bookmark;

In above code bookmark is the bookmark that needs to be referenced to page numbers so that on click of the page number it has to jump to that particular bookmark.

Regards,
Chetan

@KCSR I am afraid TOA field does not allow to achieve this. Please See TOA field codes for more information:
https://support.microsoft.com/en-us/office/field-codes-toa-table-of-authorities-c754a963-90c1-4d32-b2db-1ed90b9dd958

Probably in your case you should use TOC, that has \h switch, that makes TOC items clickable:
https://support.microsoft.com/en-us/office/field-codes-toc-table-of-contents-field-1f538bc4-60e6-4854-9f64-67754d78d05c

1 Like

Thanks Alexey, Is there any way we can tweak to make that page number field in TOA clickable during the TOA creation or after the TOA creation is completed? If yes could you please share code sample using which I can achieve this?

I am afraid if I use TOC option, I might not get the TOA structure same as I showed in my last message.

Regards,
Chetan

@KCSR Unfortunately, there is no easy way to achieve this. I think it would be easier to build “like TOA” content manually with links to the corresponding TA fields. To achieve this you should collects all TA fields, insert insert bookmarks at the TA fields and then construct the “like TOA”. The simplified code can look like this:

Document doc = new Document(@"C:\Temp\in.docx");
DocumentBuilder builder = new DocumentBuilder(doc);
LayoutCollector collector = new LayoutCollector(doc);

// Collect TA fields.
List<FieldTA> taFields = doc.Range.Fields.Where(f => f.Type == FieldType.FieldTOAEntry).Cast<FieldTA>().ToList();

// Insert bookmakrs at TA field
int bkIndex = 0;
Dictionary<string, Dictionary<string, string>> tocCategories = new Dictionary<string, Dictionary<string, string>>();
foreach (FieldTA ta in taFields)
{
    string taCategory = ta.EntryCategory;
    if (!tocCategories.Keys.Contains(taCategory))
        tocCategories.Add(taCategory, new Dictionary<string, string>());

    string bkName = string.Format("_toa_entry_{0}", bkIndex);
    bkIndex++;

    // Format text of TOA entry, that consis of long citation tab and page number.
    string toaEntryText = string.Format("{0}\t{1}", ta.LongCitation, collector.GetStartPageIndex(ta.Start));

    tocCategories[taCategory].Add(bkName, toaEntryText);

    // insert bookmark.
    builder.MoveToField(ta, true);
    builder.StartBookmark(bkName);
    builder.EndBookmark(bkName);
}

// Now move document builder at the location where TOA should be build
// For demonstration purposes move to the beginning of the document.
builder.MoveToDocumentStart();
double tabPosition = builder.PageSetup.PageWidth - builder.PageSetup.RightMargin - builder.PageSetup.LeftMargin;

// Construct the like TOA content.
foreach (string catogory in tocCategories.Keys)
{
    builder.ParagraphFormat.ClearFormatting();
    builder.Font.ClearFormatting();
    builder.Font.Size = 14;
    builder.Font.Bold = true;
    builder.Writeln(catogory);

    builder.Font.ClearFormatting();
    builder.ParagraphFormat.TabStops.Clear();
    builder.ParagraphFormat.TabStops.Add(tabPosition, TabAlignment.Right, TabLeader.Dots);
    foreach (string bkName in tocCategories[catogory].Keys)
    {
        builder.InsertHyperlink(tocCategories[catogory][bkName], bkName, true);
        builder.Writeln();
    }
}

doc.Save(@"C:\Temp\out.docx");
1 Like

Thanks Alexey, this was helpful!

One last query, If I save the document after constructing and inserting TOA to document, the final document looks good but if I convert that document to ooxml(code below) then the document is having more space between the lines in TOA constructed region in other words the styles are getting changed. In this case our page numbers calculated in TOA constructed section will go wrong since the number of pages also increases because of introducing more spaces. Any suggestions on this?

MemoryStream dstStream = new MemoryStream();
doc.Save(dstStream, Aspose.Words.SaveFormat.Docx);
doc.Save("C:\\InsertTOADocument.docx");
using (WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Open(dstStream, true))
{
      string ooxml = wordprocessingDocument.ToFlatOpcString();
      return ooxml;
}

Regards,
Chetan

@KCSR If you need to convert document to flat OPC string, you can achieve this directly from Aspose.Words without using WordprocessingDocument:

private static string DocumentToFlatOpcString(Document doc)
{
    using (MemoryStream flatOpcStream = new MemoryStream())
    {
        doc.Save(flatOpcStream, SaveFormat.FlatOpc);
        flatOpcStream.Position = 0;
        return Encoding.UTF8.GetString(flatOpcStream.ToArray());
    }
}

Thanks Alexey, I tried with your suggested ‘DocumentToFlatOpcString’ and still I see some difference in space between the lines and also if you observe the below screenshots font set for “Abdulhaseeb v. Calbone,” and “Allen v. Muskogee,” was TimesRoman before converting the DocumentToFlatOpcString and After Converting it is Calibri.

Before DocumentToFlatOpcString -

After DocumentToFlatOpcString -

Regards,
Chetan

Alexey, I had not set the Font name for “Abdulhaseeb v. Calbone,” and “Allen v. Muskogee,” explicitly and I think because of which the Font name was changing to default name i.e., Calibri.

Now the problem is only with the space between lines and this added spaces after the ‘DocumentToFlatOpcString’ convertion will move the content and TOA pages will be showed wrong in this case. Could you please let me know if we can set this space between the lines also?

I am using builder.InsertBreak(BreakType.ParagraphBreak); and builder.InsertBreak(BreakType.LineBreak);

Do i need to set any styling for this so that the space between the lines remains same in both the cases?

Hi Alexey,

I did try using LineSpacingRule and LineSpacing and was able to reduce the space between line or achieve uniform spacing.

builder.ParagraphFormat.LineSpacingRule = LineSpacingRule.Exactly;
builder.ParagraphFormat.LineSpacing = 12;

But I see there is some space inserted just after the paragraph text - after converting document to FlatOpcString using ‘DocumentToFlatOpcString’. This Space is inserted only for TOA Headings and not for TOA Entries.

In Word there is “Remove Space After Paragraph” option (below screenshot) under “Line and Paragraph Spacing” settings

Could you please let me know how do we set the “Remove Space After Paragraph” for builder in Aspose? If we set this option we should be good.

Regards,
Chetan

@KCSR To remove spacing after the paragraph, you can set ParagraphFormat.SpaceAfter property of the appropriate paragraph to zero.

When you use InsertBreak(BreakType.ParagraphBreak) it insert a paragraph break, when you use InsertBreak(BreakType.LineBreak) it inserts a soft line breaks. In the later case the lines are in the same paragraph and spacing between these lines is controlled by ParagraphFormat.LineSpacingRule and ParagraphFormat.LineSpacing properties. While spacing between paragraphs is controlled by ParagraphFormat.SpaceAfter and ParagraphFormat.SpaceBefore properties.

Also, could you please clarify - the constructed TOA looks properly when you save the document to DOCX, but if save the same document as FlatOPC it looks improperly, right? If so, could you please attach output DOCX and FlatOpc documents produced on your side?

1 Like

Thanks a lot Alexey for the information, was able to resolve that issue. Thanks for being with me to accomplish this requirement!! :smiling_face_with_three_hearts:

1 Like