Search code examples
c#ms-wordvstofind-replaceword-interop

Word Interop: How do I set an insertion point based on the results of a Find command?


Note: Code is based on the approach documented in Using a macro to replace text where ever it appears in a document

My goal is to insert a new paragraph immediately after a word found in the first page header using Word Interop Find.Execute. To do this, I need an insertion point (type == wdSelectionIP) at either the beginning or the end of the found word.

My assumption is/was that as a result of finding the word in the first page header using Word Interop Find.Execute, Word will set an insertion point (type == wdSelectionIP) at either the beginning or at the end of the found word. You can see this in my SomeEventMethod_Click method, i.e., under this assumption, after I find the word, I navigate to the end of the line, create a new, empty paragraph, set some attributes, then type in some text.

The text is typed in, but it's not following the word found in the first page header. Rather, the text is typed in the main text area (i.e., the body of the document) at the bottom of the last page.

How do I set an insertion point based on the results of a Find command?

Class use to report find and replace results

private class ClsFindReplaceResults
{
    bool isFound = false;
    Microsoft.Office.Interop.Word.Selection selection = null;

    public ClsFindReplaceResults(bool isFound, Selection selection)
    {
        this.IsFound = isFound;
        this.Selection = selection;
    }

    public bool IsFound { get => isFound; set => isFound = value; }
    public Selection Selection { get => selection; set => selection = value; }
}

Event method from which FindReplaceAnywhere method is called

private void SomeEventMethod_Click(object sender, RibbonControlEventArgs e)
{
    //Find the text 'foo\r'. No replacement. I just want the insertion point
    ClsFindReplaceResults objFindReplaceResults = FindReplaceAnywhere(findText: "foo^p", replaceWithText: null, enumWdStoryType: WdStoryType.wdFirstPageHeaderStory);

    if (objFindReplaceResults.IsFound)
    {
        objFindReplaceResults.Selection.EndKey(WdUnits.wdStory);
        objFindReplaceResults.Selection.TypeParagraph();
        objFindReplaceResults.Selection.Font.Size = 9;
        objFindReplaceResults.Selection.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphJustify;
        objFindReplaceResults.Selection.ParagraphFormat.SpaceAfter = 6f;
        objFindReplaceResults.Selection.TypeText("new paragraph that should appear after 'foo^p'");
    }
}

FindReplaceAnywhere method

private ClsFindReplaceResults FindReplaceAnywhere(string findText, string replaceWithText, WdStoryType enumWdStoryType)
{
    bool found = false;
    object wfrFindText = findText;
    object wfrMatchCase = true;
    object wfrMatchWholeWord = true;
    object wfrMatchWildCards = false;
    object wfrMatchSoundsLike = false;
    object wfrMatchAllWordForms = false;
    object wfrForward = true;
    object wfrWrap = WdFindWrap.wdFindContinue;
    object wfrFormat = false;
    object wfrReplaceWith = replaceWithText;
    object wfrReplace = null;

    if (wfrReplaceWith == null)
    {
        wfrReplace = WdReplace.wdReplaceNone;
    }
    else
    {
        wfrReplace = WdReplace.wdReplaceOne;
    }

    object wfrMatchKashida = false;
    object wfrMatchDiacritics = false;
    object wfrMatchAlefHamza = false;
    object wfrMatchControl = false;

    Globals.ThisAddIn.Application.Selection.Find.ClearFormatting();
    Globals.ThisAddIn.Application.Selection.Find.Replacement.ClearFormatting();

    //Fix the skipped blank Header/Footer problem as provided by Peter Hewett. Don't know what the heck this does
    WdStoryType junk = Globals.ThisAddIn.Application.ActiveDocument.Sections[1].Headers[Microsoft.Office.Interop.Word.WdHeaderFooterIndex.wdHeaderFooterPrimary].Range.StoryType;

    Microsoft.Office.Interop.Word.Range workingStoryRange = null;

    foreach (Microsoft.Office.Interop.Word.Range storyRange in Globals.ThisAddIn.Application.ActiveDocument.StoryRanges)
    {
        if (storyRange.StoryType != enumWdStoryType)
        {
            continue;
        }

        workingStoryRange = storyRange;

        do
        {
            // Find and replace text in the current story
            found = workingStoryRange.Find.Execute(FindText: ref wfrFindText, MatchCase: ref wfrMatchCase, MatchWholeWord: ref wfrMatchWholeWord, MatchWildcards: ref wfrMatchWildCards, MatchSoundsLike: ref wfrMatchSoundsLike, MatchAllWordForms: ref wfrMatchAllWordForms, Forward: ref wfrForward, Wrap: ref wfrWrap, Format: ref wfrFormat, ReplaceWith: ref wfrReplaceWith, Replace: ref wfrReplace, MatchKashida: ref wfrMatchKashida, MatchDiacritics: ref wfrMatchDiacritics, MatchAlefHamza: ref wfrMatchAlefHamza, MatchControl: ref wfrMatchControl);

            // The call to SearchAndReplaceInStory above misses text that is contained in a StoryType/StoryRange nested in a different 
            // StoryType /StoryRange. While this won't occur with a nested StoryType/StoryRange in the wdMainTextStory StoryRange, it 
            // will occur in header and footer type StoryRanges. An example is textbox that is located in a header or footer. The fix 
            // makes use of the fact that Textboxes and other Drawing Shapes are contained in a document’s ShapeRange collection. 
            // Check the ShapeRange in each of the six header and footer StoryRanges for the presence of Shapes. If a Shape is found, 
            // check each Shape for the presence of the text, and finally, if the Shape contains text we set our search range to that 
            // Shape's .TextFrame.TextRange. 
            switch (workingStoryRange.StoryType)
            {
                // Case 6 , 7 , 8 , 9 , 10 , 11
                case Microsoft.Office.Interop.Word.WdStoryType.wdEvenPagesHeaderStory:
                case Microsoft.Office.Interop.Word.WdStoryType.wdPrimaryHeaderStory:
                case Microsoft.Office.Interop.Word.WdStoryType.wdFirstPageHeaderStory:
                case Microsoft.Office.Interop.Word.WdStoryType.wdEvenPagesFooterStory:
                case Microsoft.Office.Interop.Word.WdStoryType.wdPrimaryFooterStory:
                case Microsoft.Office.Interop.Word.WdStoryType.wdFirstPageFooterStory:

                    if (workingStoryRange.ShapeRange.Count > 0)
                    {
                        foreach (Microsoft.Office.Interop.Word.Shape shape in workingStoryRange.ShapeRange)
                        {
                            if (shape.TextFrame.HasText != 0)
                            {
                                found = shape.TextFrame.TextRange.Find.Execute(FindText: ref wfrFindText, MatchCase: ref wfrMatchCase, MatchWholeWord: ref wfrMatchWholeWord, MatchWildcards: ref wfrMatchWildCards, MatchSoundsLike: ref wfrMatchSoundsLike, MatchAllWordForms: ref wfrMatchAllWordForms, Forward: ref wfrForward, Wrap: ref wfrWrap, Format: ref wfrFormat, ReplaceWith: ref wfrReplaceWith, Replace: ref wfrReplace, MatchKashida: ref wfrMatchKashida, MatchDiacritics: ref wfrMatchDiacritics, MatchAlefHamza: ref wfrMatchAlefHamza, MatchControl: ref wfrMatchControl);
                            }
                        }
                    }

                    break;

                default:
                    break;
            }

            workingStoryRange = workingStoryRange.NextStoryRange;

        } while (workingStoryRange != null);
    }

    return new ClsFindReplaceResults(found, Globals.ThisAddIn.Application.Selection);
}

Solution

  • The code in the question is searching on a Range object, so the Selection won't change. Simply use the Range as the "target" for the new content.

    The code in the quesiton is so complex it's hard to follow exactly what's going on... But in simple terms:

    bool found = workingStoryRange.Find.Execute(FindText: ref wfrFindText, MatchCase: ref wfrMatchCase, 
      MatchWholeWord: ref wfrMatchWholeWord, MatchWildcards: ref wfrMatchWildCards, MatchSoundsLike: ref wfrMatchSoundsLike, 
      MatchAllWordForms: ref wfrMatchAllWordForms, Forward: ref wfrForward, Wrap: ref wfrWrap, Format: ref wfrFormat, 
      ReplaceWith: ref wfrReplaceWith, Replace: ref wfrReplace, MatchKashida: ref wfrMatchKashida, 
      MatchDiacritics: ref wfrMatchDiacritics, MatchAlefHamza: ref wfrMatchAlefHamza, MatchControl: ref wfrMatchControl);
    
    if (found)
    {
      //Work with a duplicate of the original range so as not to "destroy" it
      //may not be needed, but included for "in case"
      Word.Range rngFound = workingStoryRange.Duplicate;
      //go to the end - the point just after the found content
      rngFound.Collapse(Word.WdCollapseDirection.wdCollapseEnd);
      rngFound = "\nText in new paragraph.";
      rngFound.Font.Size = 9
      rngFound.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphJustify;
      rngFound.ParagraphFormat.SpaceAfter = 6f;
    }
    

    Note: it would be more correct to create a style for this formatting. The formatting can then be applied in one step, as often as required. A style has the following advantages

    • If the formatting should change at a later time, it's a simple matter to change the style definition - in one place, one action - rather than needing to find and change each formatting instance in the document
    • Applying formatting using a style reduces the amount of memory management, most especially the "scratch files" Word uses to maintain the "Undo list". A corralary: the Undo list is shorter.