Add hyperlink to textblock wpf

52,109

Solution 1

You can use Regex with a value converter in such situation.

Use this for your requirements (original idea from here):

    private Regex regex = 
        new Regex(@"\[a\s+href='(?<link>[^']+)'\](?<text>.*?)\[/a\]",
        RegexOptions.Compiled);

This will match all links in your string containing links, and make 2 named groups for each match : link and text

Now you can iterate through all the matches. Each match will give you a

    foreach (Match match in regex.Matches(stringContainingLinks))
    { 
        string link    = match.Groups["link"].Value;
        int link_start = match.Groups["link"].Index;
        int link_end   = match.Groups["link"].Index + link.Length;

        string text    = match.Groups["text"].Value;
        int text_start = match.Groups["text"].Index;
        int text_end   = match.Groups["text"].Index + text.Length;

        // do whatever you want with stringContainingLinks.
        // In particular, remove whole `match` ie [a href='...']...[/a]
        // and instead put HyperLink with `NavigateUri = link` and
        // `Inlines.Add(text)` 
        // See the answer by Stanislav Kniazev for how to do this
    }

Note : use this logic in your custom ConvertToHyperlinkedText value converter.

Solution 2

Displaying is rather simple, the navigation is another question. XAML goes like this:

<TextBlock Name="TextBlockWithHyperlink">
    Some text 
    <Hyperlink 
        NavigateUri="http://somesite.com"
        RequestNavigate="Hyperlink_RequestNavigate">
        some site
    </Hyperlink>
    some more text
</TextBlock>

And the event handler that launches default browser to navigate to your hyperlink would be:

private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) {
    System.Diagnostics.Process.Start(e.Uri.ToString());
}

Edit: To do it with the text you've got from database you'll have to parse the text somehow. Once you know the textual parts and hyperlinked parts, you can build textblock contents dynamically in the code:

TextBlockWithHyperlink.Inlines.Clear();
TextBlockWithHyperlink.Inlines.Add("Some text ");
Hyperlink hyperLink = new Hyperlink() {
    NavigateUri = new Uri("http://somesite.com")
};
hyperLink.Inlines.Add("some site");
hyperLink.RequestNavigate += Hyperlink_RequestNavigate;
TextBlockWithHyperlink.Inlines.Add(hyperLink);
TextBlockWithHyperlink.Inlines.Add(" Some more text");

Solution 3

Another version of this and not totally the same as recognizing the format here, but here is a class for automatically recognizing links in a piece of text and making them live hyperlinks:

internal class TextBlockExt
{
    static Regex _regex =
        new Regex(@"http[s]?://[^\s-]+",
                  RegexOptions.Compiled);

    public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached("FormattedText", 
        typeof(string), typeof(TextBlockExt), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure, FormattedTextPropertyChanged));
    public static void SetFormattedText(DependencyObject textBlock, string value)
    { textBlock.SetValue(FormattedTextProperty, value); }

    public static string GetFormattedText(DependencyObject textBlock)
    { return (string)textBlock.GetValue(FormattedTextProperty); }

    static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is TextBlock textBlock)) return; 

        var formattedText = (string)e.NewValue ?? string.Empty;
        string fullText =
            $"<Span xml:space=\"preserve\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">{formattedText}</Span>";

        textBlock.Inlines.Clear();
        using (var xmlReader1 = XmlReader.Create(new StringReader(fullText)))
        {
            try
            {
                var result = (Span)XamlReader.Load(xmlReader1);
                RecognizeHyperlinks(result);
                textBlock.Inlines.Add(result);
            }
            catch
            {
                formattedText = System.Security.SecurityElement.Escape(formattedText);
                fullText =
                    $"<Span xml:space=\"preserve\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">{formattedText}</Span>";

                using (var xmlReader2 = XmlReader.Create(new StringReader(fullText)))
                {
                    try
                    {
                        dynamic result = (Span) XamlReader.Load(xmlReader2);
                        textBlock.Inlines.Add(result);
                    }
                    catch
                    {
                        //ignored
                    }
                }
            }
        }
    }

    static void RecognizeHyperlinks(Inline originalInline)
    {
        if (!(originalInline is Span span)) return;

        var replacements = new Dictionary<Inline, List<Inline>>();
        var startInlines = new List<Inline>(span.Inlines);
        foreach (Inline i in startInlines)
        {
            switch (i)
            {
                case Hyperlink _:
                    continue;
                case Run run:
                {
                    if (!_regex.IsMatch(run.Text)) continue;
                    var newLines = GetHyperlinks(run);
                    replacements.Add(run, newLines);
                    break;
                }
                default:
                    RecognizeHyperlinks(i);
                    break;
            }
        }

        if (!replacements.Any()) return;

        var currentInlines = new List<Inline>(span.Inlines);
        span.Inlines.Clear();
        foreach (Inline i in currentInlines)
        {
            if (replacements.ContainsKey(i)) span.Inlines.AddRange(replacements[i]);
            else span.Inlines.Add(i);
        }
    }

    static List<Inline> GetHyperlinks(Run run)
    {
        var result = new List<Inline>();
        var currentText = run.Text;
        do
        {
            if (!_regex.IsMatch(currentText))
            {
                if (!string.IsNullOrEmpty(currentText)) result.Add(new Run(currentText));
                break;
            }
            var match = _regex.Match(currentText);

            if (match.Index > 0)
            {
                result.Add(new Run(currentText.Substring(0, match.Index)));
            }

            var hyperLink = new Hyperlink() { NavigateUri = new Uri(match.Value) };
            hyperLink.Inlines.Add(match.Value);
            hyperLink.RequestNavigate += HyperLink_RequestNavigate;
            result.Add(hyperLink);

            currentText = currentText.Substring(match.Index + match.Length);
        } while (true);

        return result;
    }

    static void HyperLink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
    {
        try
        {
            Process.Start(e.Uri.ToString());
        }
        catch { }
    }
}

Using that you can just do <TextBlock ns:TextBlockExt.FormattedText="{Binding Content}" /> instead of <TextBlock Text="{Binding Content}" /> and it will recognize and activate links automatically, as well as recognizing normal formatting tags like <Bold>, etc.

Note that this is based off of the answer by @gwiazdorrr here as well as some other answers on this question; I basically combined them all into 1 and did some recursion handling and it works! :). The patterns and systems could also be adapted to recognize other types of links or markup if desired.

Share:
52,109
niao
Author by

niao

"the limitation is your imagination"

Updated on July 09, 2022

Comments

  • niao
    niao almost 2 years

    Greetings, I have some text in a db and it is as follows:

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis tellus nisl, venenatis et pharetra ac, tempor sed sapien. Integer pellentesque blandit velit, in tempus urna semper sit amet. Duis mollis, libero ut consectetur interdum, massa tellus posuere nisi, eu aliquet elit lacus nec erat. Praesent a commodo quam. **[a href='http://somesite.com']some site[/a]**Suspendisse at nisi sit amet massa molestie gravida feugiat ac sem. Phasellus ac mauris ipsum, vel auctor odio

    My question is: How can I display a Hyperlink in a TextBlock? I don't want to use a webBrowser control for this purpose. I don't want to use this control either: http://www.codeproject.com/KB/WPF/htmltextblock.aspx also

  • niao
    niao over 14 years
    yes..but as I wrote I have this link included in some text that is stored in db. I would like then to read the text and add appropriate hyperlinks when needed
  • Jippers
    Jippers over 10 years
    How would this be done in a Converter that binds the database to the TextBlock.Text?
  • Asteroids With Wings
    Asteroids With Wings about 4 years
    Do you mean <TextBlock ns:TextBlockExt.FormattedText="{Binding Content}" />?
  • Asteroids With Wings
    Asteroids With Wings about 4 years
    "http[s]?://[^\s-]+" Not quite sure I understand why - is not treated as a valid part of a URI?
  • Asteroids With Wings
    Asteroids With Wings about 4 years
    Also I had to replace !replacements.Any() with replacements.Count == 0; perhaps the original code is using the Linq dictionary though. Hard to tell without namespaces given
  • sfaust
    sfaust about 4 years
    To be honest I'm ok with regex but not a regex master by any means; it's been a while but I believe I pulled that pattern from somewhere else but don't remember where anymore. It's certainly possible it could use a little tweaking but has been working for what I've used it for so far. As far as Any() yes that's a LINQ expression it's using.
  • Asteroids With Wings
    Asteroids With Wings about 4 years
    For what it's worth, I'm using @"http[s]?://[^\s]*[^\s\.]". It won't eat up a trailing period before a space (or before the end of the string), on the assumption that said period terminates a sentence rather than terminating the URI. But whether this is appropriate depends on the use case. And I took away the - exemption; there are hyphens in URIs all over the place (including in the URI for this very page :P)
  • tok3rat0r
    tok3rat0r over 2 years
    Note to those who find this answer but get a 'FileNotFound' exception: I had to replace the process start line with System.Diagnostics.Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true });