views:

1081

answers:

4

About to go mad with this problem. I'm sure it's so simple I'm just missing it, but I cannot for the life of me find out how to change the content of a content control in Word 2007 with the OpenXml SDK v2.0 in C#.

I have created a Word document with a plain text content control. The tag for this control is "FirstName". In code, I'd like to open up the Word document, find this content control, and change the content without losing the formatting.

The solution I finally got to work involved finding the content control, inserting a run after it, then removing the content control as such:

using (WordprocessingDocument wordProcessingDocument = WordprocessingDocument.Open(filePath, true)) {
MainDocumentPart mainDocumentPart = wordProcessingDocument.MainDocumentPart;
SdtRun sdtRun = mainDocumentPart.Document.Descendants<SdtRun>()
 .Where(run => run.SdtProperties.GetFirstChild<Tag>().Val == "FirstName").Single();

if (sdtRun != null) {
 sdtRun.Parent.InsertAfter(new Run(new Text("John")), sdtRun);
 sdtRun.Remove();
}

This does change the text, but I lose all formatting. Does anyone know how I can do this?

A: 

I found a better way to do the above using http://wiki.threewill.com/display/enterprise/SharePoint+and+Open+XML#SharePointandOpenXML-UsingWord2007ContentControls as a reference. Your results may vary but I think this will get you off to a good start:

using (WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Open(filePath, true)) {
    var sdtRuns = mainDocumentPart.Document.Descendants<SdtRun>()
        .Where(run => run.SdtProperties.GetFirstChild<Tag>().Val.Value == contentControlTagValue);

    foreach (SdtRun sdtRun in sdtRuns) {
        sdtRun.Descendants<Text>().First().Text = replacementText;
    }

    wordprocessingDocument.MainDocumentPart.Document.Save();
}

I think the above will only work for Plain Text content controls. Unfortunately, it doesn't get rid of the content control in the final document. If I get that working I'll post it.

http://msdn.microsoft.com/en-us/library/cc197932.aspx is also a good reference if you want to find a rich text content control. This one talks about adding rows to a table that was placed in a rich text content control.

Jason
It looks like this solution won't work if the content control is the only item in a paragraph (i.e. is not surrounded by other text). As a quick work around I just put a space on one side of the content control. I will post a better solution when I find one.
Jason
+1  A: 

Your first approach to remove the sdtRun and adding a new one will obviously remove the formatting because you are only adding a Run but not the RunStyle. To preserve the formatting you should create run elements like

new Run( new RunProperties(new RunStyle(){ Val = "MyStyle" }),
                            new Text("Replacement Text"));

Your second approach to replace all Decendants<Text> will work for Plain Text Content Control only because a Rich Text Content Control does not have SdtRun element. Rich Text Content Control is SdtBlock with SdtContent elements. A rich text content control can have multiple paragraphs, multiple Runs and multiple Texts. So your code, sdtRun.Descendants<Text>().First().Text = replacementText, will be flawed for a Rich Text Content Control. There is no one line code to replace the entire text of a rich content control and yet preserve all the formatting.

I did not understand what you mean by "it doesn't get rid of the content control in the final document"? I thought your requirement here is to change the text (content) only by preserving the content control and the formatting.

Bijay Kusle
A: 

I also had to find and replace text in the footers. You can find them using the following code:

using (WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Open(file.PhysicalFile.FullName, true)) {
    foreach (FooterPart footerPart in wordprocessingDocument.MainDocumentPart.FooterParts) {
        var footerPartSdtRuns = footerPart.Footer.Descendants<SdtRun>()
            .Where(run => run.SdtProperties.GetFirstChild<Tag>().Val.Value == contentControlTag);

        foreach (SdtRun sdtRun in footerPartSdtRuns) {
           sdtRun.Descendants<Text>().First().Text = replacementTerm;
        }
    }

    wordprocessingDocument.MainDocumentPart.Document.Save();
}
Jason
+1  A: 

One EXCELLENT way to work out how to achieve the desired result is to use the document reflector tool that comes with the Open XML SDK 2.0....

For example, you could:

  1. In the Properties dialog for each of the content controls in your document, check the "Remove content control when the contents are edited".
  2. Fill them in and save it as a new doc.
  3. Use the reflector to compare the original and the saved version.
  4. Hit the show/hide code button and it will show you the code required to turn the original into the filled in version.

It's not perfect, but it's amazingly useful. You can also just look directly at the markup of either document and see the changes that filling in the controls caused.

This is a somewhat brittle way to do it because Wordprocessing ML is can be complicated; it's easy to mess it up. For simple text controls, I just use this method:

private void FillSimpleTextCC(SdtRun simpleTextCC, string replacementText)
    {
        // remove the showing place holder element      
        SdtProperties ccProperties = simpleTextCC.SdtProperties;
        ccProperties.RemoveAllChildren<ShowingPlaceholder>();

        // fetch content block Run element            
        SdtContentRun contentRun = simpleTextCC.SdtContentRun;
        var ccRun = contentRun.GetFirstChild<Run>();

        // if there was no placeholder text in the content control, then the SdtContentRun
        // block will be empty -> ccRun will be null, so create a new instance
        if (ccRun == null)
        {
            ccRun = new Run(
                new RunProperties() { RunStyle = null },
                new Text());
            contentRun.Append(ccRun);
        }

        // remove revision identifier & replace text
        ccRun.RsidRunProperties = null;
        ccRun.GetFirstChild<Text>().Text = replacementText;

        // set the run style to that stored in the SdtProperties block, if there was
        // one. Otherwise the existing style will be used.            
        var props = ccProperties.GetFirstChild<RunProperties>();
        if (props != null)
        if (props != null)
        {
            RunStyle runStyle = props.RunStyle;
            if (runStyle != null)
            {
                // set the run style to the same as content block property style.
                var runProps = ccRun.RunProperties;
                runProps.RunStyle = new RunStyle() { Val = runStyle.Val };
                runProps.RunFonts = null;
            }
        }
    }

Hope that helps in some way. :D

Got pulled off this for a few days, but I'll revisit it soon and look into the above suggestions. Thanks
Jason