1

enter image description here

I want to remove all hyperlinks located in a RichTextBox but not the runs inside the hyperlinks.

My plan is for each hyperlink:

  • gather all runs inside it
  • remove the hyperlink
  • re-insert the runs

While extracting the runs I face this problem: If the hyperlink consists of one unformatted run, I don't get the run but also the surrounding hyperlink.

Please try this code:

Xaml:

<RichTextBox Name="rtb" Grid.Column="0" Grid.Row="0" IsDocumentEnabled="True">
    <FlowDocument>
        <Paragraph>
            <Hyperlink>
                <Run>HyperlinkUnformatted</Run>
            </Hyperlink>
        </Paragraph>
        <Paragraph>
            <Hyperlink>
                <Run>Hyper</Run><Run Background="Yellow">link</Run><Run>Formatted</Run>
            </Hyperlink>
        </Paragraph>
    </FlowDocument>
</RichTextBox> 

C#:

// All runs inside the RichTextBox
List<Run> runs = runsGet(rtb.Document.ContentStart, rtb.Document.ContentEnd);

foreach (Run run in runs)
{
    TextRange rangeOfRun = new TextRange(run.ContentStart, run.ContentEnd);
    string runAsString = rangeToString(rangeOfRun, DataFormats.Xaml);
    MessageBox.Show(runAsString);
}

/// <summary>
/// Returns all runs between startPos and endPos.
/// </summary>
private List<Run> runsGet(TextPointer startPos, TextPointer endPos)
{
    List<Run> foundRuns = null;
    TextPointer currentPos = startPos;

    while (currentPos != null && currentPos.CompareTo(endPos) <= 0)
    {
        Run nextRun = runNextGet(currentPos);
        if (nextRun == null) break;

        if (nextRun.ContentStart.CompareTo(endPos) <= 0)
        {
            if (foundRuns == null) foundRuns = new List<Run>();

            foundRuns.Add(nextRun);

            currentPos = nextRun.ContentEnd.GetNextInsertionPosition(LogicalDirection.Forward);
        }
        else
        {
            break;
        }
    }

    return foundRuns;
}

/// <summary>
/// Returns first run located at startPos or behind.
/// </summary>
private Run runNextGet(TextPointer startPos)
{
    TextPointer currentPos = startPos;

    while (currentPos != null)
    {
        if (currentPos.Parent is Run)
        {
            return currentPos.Parent as Run;
        }
        else
        {
            currentPos = currentPos.GetNextInsertionPosition(LogicalDirection.Forward);
        }
    }

    return null;
}

/// <summary>
/// Returns a text area as Xaml string (if dataFormat is DataFormats.Xaml).
/// </summary>
private string rangeToString(TextRange range, string dataFormat)
{
    using (MemoryStream memStream = new MemoryStream())
    {
        using (StreamWriter streamWriter = new StreamWriter(memStream))
        {
            range.Save(memStream, dataFormat);

            memStream.Flush();
            memStream.Position = 0;

            StreamReader streamReader = new StreamReader(memStream);

            return streamReader.ReadToEnd();
        }
    }
}
Pollitzer
  • 1,580
  • 3
  • 18
  • 28
  • See the answer to the [How to get HyperLink Text from C# in WPF?](http://stackoverflow.com/questions/19645110/how-to-get-hyperlink-text-from-c-sharp-in-wpf) question. – Sheridan Sep 15 '14 at 09:39

1 Answers1

0

What you want to do is, for each Hyperlink, copy the Inlines of the Hyperlink into a separate XamlPackage, then paste them back into the FlowDocument, overwriting the Hyperlink itself. Unfortunately, a naive implementation of this will fail because TextRange.Save() "helpfully" expands the selected range to the nearest text insertion boundaries. Since the space between a hyperlink and the beginning of its first inline, being of size zero, is not a valid caret position and thus not a valid insertion position, the text range gets expanded to include the Hyperlink. It is thereby included in the package and gets recreated when pasted back in.

To work around this behavior, you can insert some additional dummy text before and after the existing Inlines:

public static class FlowDocumentHelper
{
    public static IEnumerable<DependencyObject> WalkTreeDown(this DependencyObject root)
    {
        if (root == null)
            throw new ArgumentNullException();
        yield return root;
        foreach (var child in LogicalTreeHelper.GetChildren(root).OfType<DependencyObject>())
            foreach (var descendent in child.WalkTreeDown())
                yield return descendent;
    }

    public static IEnumerable<DependencyObject> WalkTreeUp(this DependencyObject child)
    {
        for (; child != null; child = LogicalTreeHelper.GetParent(child))
            yield return child;
    }

    public static void RemoveHyperlinks(this FlowDocument doc)
    {
        var allLinks = doc.WalkTreeDown().OfType<Hyperlink>().ToArray();
        for (int iLink = allLinks.Length - 1; iLink >= 0; iLink--)
        {
            var link = allLinks[iLink];
            var range = new TextRange(link.ContentStart.GetInsertionPosition(LogicalDirection.Backward), link.ContentEnd.GetInsertionPosition(LogicalDirection.Forward));

            var first = link.Inlines.FirstInline;
            var last = link.Inlines.LastInline;

            link.Inlines.Add(new Run(" "));
            link.Inlines.InsertBefore(first, new Run(" "));

            var childRange = new TextRange(first.ContentStart, last.ContentEnd);

            using (MemoryStream ms = new MemoryStream())
            {
                string format = DataFormats.XamlPackage;

                childRange.Save(ms, format, true);
                ms.Seek(0, SeekOrigin.Begin);
                range.Load(ms, format);
            }
        }
    }
}

I've tested this with your case and also with a Hyperlink containing an embedded image.

Of course, the underlining and formatting from the Hyperlink itself are removed. You could add them back afterward if you want.

dbc
  • 104,963
  • 20
  • 228
  • 340