Problems retrieving content controls with Open XML

2019-08-11 02:58发布

问题:

I am developing a solution that will generate word-documents. The word-documents are generated on the basis of a template document which has defined content controls. Everything was working good for me when I had only one content control in my template, but after expanding the template document with more content controls, I am getting exceptions. It seems like I am not finding the content controls.

This is my method:

private void CreateReport(File file)

    {
        var byteArray = file.OpenBinary();
        using (var mem = new MemoryStream())
        {
            mem.Write(byteArray, 0, byteArray.Length);
            try
            {
                using (var wordDoc = WordprocessingDocument.Open(mem, true))
                {
                    var mainPart = wordDoc.MainDocumentPart;

                    var firstName = mainPart.Document.Body.Descendants<SdtBlock>().Where
                        (r => r.SdtProperties.GetFirstChild<Tag>().Val == "FirstName").Single();
                    var t = firstName.Descendants<Text>().Single();
                    t.Text = _firstName;

                     var lastName = mainPart.Document.Body.Descendants<SdtBlock>().Where
                        (r => r.SdtProperties.GetFirstChild<Tag>().Val == "LastName").Single();
                     var t2= lastName.Descendants<Text>().Single();
                     t2.Text = _lastName;

                    mainPart.Document.Save();
                    SaveFileToSp(mem);
                }

            }
            catch (FileFormatException)
            {
            }
        }
    }

This is the exception I get:

An exception of type 'System.InvalidOperationException' occurred in System.Core.dll but was not handled in user code. Innerexception: Null

Any tips for me on how I can write better method for finding controls?

回答1:

Your issue is that one (or more) of your calls to Single() is being called on a sequence that has more than one element. The documentation for Single() states (emphasis mine):

Returns the only element of a sequence, and throws an exception if there is not exactly one element in the sequence.

In your code this can happen in one of two scenarios. The first is if you have more than one control with the same Tag value, for example you might have two controls in the document labelled "LastName" which would mean that this line

var lastName = mainPart.Document.Body.Descendants<SdtBlock>().Where
                    (r => r.SdtProperties.GetFirstChild<Tag>().Val == "LastName")

would return two elements.

The second is if your content control has more than one Text element in it in which case this line

var t = firstName.Descendants<Text>();

would return multiple elements. For example if I create a control with the content "This is a test" I end up with XML which has 4 Text elements:

<w:p w:rsidR="00086A5B" w:rsidRDefault="003515CB">
    <w:r>
        <w:rPr>
            <w:rStyle w:val="PlaceholderText" />
        </w:rPr>
        <w:t xml:space="preserve">This </w:t>
    </w:r>
    <w:r>
        <w:rPr>
            <w:rStyle w:val="PlaceholderText" />
            <w:i />
        </w:rPr>
        <w:t>is</w:t>
    </w:r>
    <w:r>
        <w:rPr>
            <w:rStyle w:val="PlaceholderText" />
        </w:rPr>
        <w:t xml:space="preserve"> </w:t>
    </w:r>
    <w:r w:rsidR="00E1178E">
        <w:rPr>
            <w:rStyle w:val="PlaceholderText" />
        </w:rPr>
        <w:t>a test</w:t>
    </w:r>
</w:p>

How to get round the first issue depends on whether you wish to replace all of the matching Tag elements or just one particular one (such as the first or last).

If you want to replace just one you can change the call from Single() to First() or Last() for example but I guess you need to replace them all. In that case you need to loop around each matching element for each tag name you wish to replace.

Removing the call to Single() will return an IEnumerable<SdtBlock> which you can iterate around replacing each one:

IEnumerable<SdtBlock> firstNameFields = mainPart.Document.Body.Descendants<SdtBlock>().Where
    (r => r.SdtProperties.GetFirstChild<Tag>().Val == "FirstName");

foreach (var firstName in firstNameFields)
{
    var t = firstName.Descendants<Text>().Single();
    t.Text = _firstName;
}

To get around the second problem is slightly more tricky. The easiest solution in my opinion is to remove all of the existing paragraphs from the content and then add a new one with the text you wish to output.

Breaking this out into a method probably makes sense as there's a lot of repeated code - something along these lines should do it:

private static void ReplaceTags(MainDocumentPart mainPart, string tagName, string tagValue)
{
    //grab all the tag fields
    IEnumerable<SdtBlock> tagFields = mainPart.Document.Body.Descendants<SdtBlock>().Where
        (r => r.SdtProperties.GetFirstChild<Tag>().Val == tagName);

    foreach (var field in tagFields)
    {
        //remove all paragraphs from the content block
        field.SdtContentBlock.RemoveAllChildren<Paragraph>();
        //create a new paragraph containing a run and a text element
        Paragraph newParagraph = new Paragraph();
        Run newRun = new Run();
        Text newText = new Text(tagValue);
        newRun.Append(newText);
        newParagraph.Append(newRun);
        //add the new paragraph to the content block
        field.SdtContentBlock.Append(newParagraph);
    }
}

Which can then be called from your code like so:

using (var wordDoc = WordprocessingDocument.Open(mem, true))
{
    var mainPart = wordDoc.MainDocumentPart;

    ReplaceTags(mainPart, "FirstName", _firstName);
    ReplaceTags(mainPart, "LastName", _lastName);

    mainPart.Document.Save();
    SaveFileToSp(mem);
}


回答2:

I think your Single() method is causing the exception.

When you got only one content control, Single() can get the only available element. But when you expand the content controls, your Single() method can cause InvalidOperationException as there are more than one element in the sequence. If this is the case, try to loop your code and take one element at a time.