Add hyperlink to textblock WPF
Asked Answered
J

4

47

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.example]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: https://www.codeproject.com/Articles/33196/WPF-Html-supported-TextBlock

Jola answered 19/1, 2010 at 10:56 Comment(0)
I
18

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.

Insupportable answered 22/1, 2010 at 16:16 Comment(0)
G
122

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

<TextBlock Name="TextBlockWithHyperlink">
    Some text
    <Hyperlink
        NavigateUri="http://somesite.example"
        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());
}

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.example")
};
hyperLink.Inlines.Add("some site");
hyperLink.RequestNavigate += Hyperlink_RequestNavigate;
TextBlockWithHyperlink.Inlines.Add(hyperLink);
TextBlockWithHyperlink.Inlines.Add(" Some more text");
Girardi answered 19/1, 2010 at 11:19 Comment(3)
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 neededJola
How would this be done in a Converter that binds the database to the TextBlock.Text?Phalangeal
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 });Irascible
I
18

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.

Insupportable answered 22/1, 2010 at 16:16 Comment(0)
T
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.

Ticonderoga answered 19/4, 2019 at 21:39 Comment(5)
Do you mean <TextBlock ns:TextBlockExt.FormattedText="{Binding Content}" />?Hyperparathyroidism
"http[s]?://[^\s-]+" Not quite sure I understand why - is not treated as a valid part of a URI?Hyperparathyroidism
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 givenHyperparathyroidism
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.Ticonderoga
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)Hyperparathyroidism
H
0

xaml:

<TextBlock x:Name="txbLink" Height="30" Width="500" Margin="0,10"/>

C#:

Regex regex = new Regex(@"(?<text1>.*?)\<a\s+href='(?<link>\[^'\]+)'\>(?<textLink>.*?)\</a\>(?<text2>.*)", RegexOptions.Compiled);
string stringContainingLinks = "Click <a href='http://somesite.example'>here</a> for download.";
foreach (Match match in regex.Matches(stringContainingLinks))
{
        string text1 = match.Groups["text1"].Value;
        string link = match.Groups["link"].Value;
        string textLink = match.Groups["textLink"].Value;
        string text2 = match.Groups["text2"].Value;

        var h = new Hyperlink();
        h.NavigateUri = new Uri(link);
        h.RequestNavigate += new RequestNavigateEventHandler(Hyperlink_RequestNavigate);
        h.Inlines.Add(textLink);
        txbLink.Inlines.Add(text1);
        txbLink.Inlines.Add(h);
        txbLink.Inlines.Add(text2);
}

private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
    Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
    e.Handled = true;
}

Hochstetler answered 1/8, 2020 at 20:19 Comment(1)
Your example won't go inside the 'foreach' loop. ie. No match is found.Handicraftsman

© 2022 - 2024 — McMap. All rights reserved.