locked
Hyperlink in TextBlock text supplied by binding

    Question

  • I have a TextBlock within a ListView that is getting its Text from my DataBinding

    <Page.Resources>        
        <DataTemplate x:Key="ResultItemTemplate">
            <Grid Height="auto">
    
                <TextBlock Text="{Binding MessageText}" />
    
            </Grid>
        </DataTemplate>
    </Page.Resources>
    
    
    <ListView Name="PublicTweetListBox" ItemTemplate="{StaticResource ResultItemTemplate}" />

    Now I can identify the links in the MessageText Binding but I want to be able to make these a clickable hyperlink to open in Internet Explorer.

    What's the best way to go about this?



    Wednesday, April 23, 2014 12:43 PM

Answers

  • Yes. For the WebView it will use NavigateToString. For other controls you'll change the OnXXXChanged() method to match. For a TextBlock we can set the Inlines property to the Inline objects (i.e. Runs and Hyperlinks) that we want.

    Here's a quick shot at using a Regexp to find the URLs and format the string as Xaml, parse the Xaml, and set the generated visual tree into the TextBlock. There may be a simpler way to parse this into Inlines, and I make no guarantees that regexp actually matches what you want. This will probably die horribly if you pass in actual XML tags: if you don't control the input make sure to quote it before parsing.

            private static void OnUrlTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                TextBlock tb = d as TextBlock;
                if (tb != null)
                {
                    string originalString = e.NewValue as string;
                    string urlPattern = @"(http(s)?://([\w+?\.\w+])+([a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)";
    
                    const string prelude = "<Paragraph xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'><Run>";
                    const string postscript = "</Run></Paragraph>";
                    const string openHyper = "</Run><Hyperlink NavigateUri=\"";
                    const string closeHyper = "</Hyperlink><Run>";
                    
                    Regex urlRegx = new Regex(urlPattern,RegexOptions.IgnoreCase);
                    MatchCollection matches = urlRegx.Matches(originalString);
                    
                    foreach (Match match in matches)
                    {
                        originalString = originalString.Replace(match.Value,
                            openHyper + match.Value + "\">" + match.Value + closeHyper);
                    }
    
                    string xaml = prelude + originalString + postscript;
                    Paragraph para = XamlReader.Load(xaml) as Paragraph;
                  
                    Inline[] inlines = new Inline[para.Inlines.Count];
                    para.Inlines.CopyTo(inlines, 0);
                    tb.Inlines.Clear();
                    para.Inlines.Clear();
                    foreach (var inline in inlines)
                    {
                       tb.Inlines.Add(inline);
                    }
                }
            }
    --Rob



    Wednesday, April 23, 2014 9:40 PM
    Owner

All replies

  • Hi,

    You can have two different DataTemplates - one with TextBlock and with HyperlinkButton. After your code logic detects that the text is URL, you can choose the appropriate template to be applied to the ListView item. This can achieved using DataTemplateSelector. See example here

    Sagar 


    Wednesday, April 23, 2014 1:16 PM
  • One method is to add a tap event to the TextBlock and then grab the text and launch it like this:

    private async void TextBlock_Tapped(object sender, RoutedEventArgs e)
    {
            // Create the URI to launch from a string.
            var uri = new Uri((sender as TextBlock).Text);
    
            // Launch the URI.
            bool success = await Windows.System.Launcher.LaunchUriAsync(uri);
            if (success)
            {
                rootPage.NotifyUser("URI launched: " + uri.AbsoluteUri, NotifyType.StatusMessage);
            }
            else
            {
                rootPage.NotifyUser("URI launch failed.", NotifyType.ErrorMessage);
            }
    }
    A better might be to use the HyperlinkButton instead of a TextBlock.

    Here are some good blog posts on different ways to do this:

    http://www.jonathanantoine.com/2013/05/30/win8xaml-how-to-create-a-textblock-with-clickables-hyperlinks-in-it/

    http://zubairahmed.net/?p=266


    http://rbrundritt.wordpress.com

    Wednesday, April 23, 2014 1:19 PM
  • Okay That would work for most instances.

    But for example if the text contains two URLs this wouldn't work.

    It would be preferable for only the URL text to be clickable not the whole field, is this possible?

    Wednesday, April 23, 2014 1:20 PM
  • The right way to do this is to include Hyperlink elements in the TextBlock. See http://msdn.microsoft.com/en-us/library/windows/apps/xaml/windows.ui.xaml.documents.hyperlink.aspx

    You will need to parse the text to extract the URIs and for the hyperlinks. This is not done automatically for you and won't be trivially bound without wrapping the code to extract and create the Hyperlink in an attached property or such.

    Do not handle the TextBlock's Tapped event for this. That will break your app's accessibility. Tapped isn't available by keyboard and it doesn't include semantic information that the control is a hyperlink so screen readers won't handle this appropriately. You can add those back in, but it'll be a lot easier to do it right in the first place.  

    --Rob

    Wednesday, April 23, 2014 1:35 PM
    Owner
  • Okay this is more what I need. However, I'm binding the string to the Text property of the TextBlock, so when I am replacing the URIs in the text for a hyperlink tag the hyperlink tag is just displayed as text.

    So how would I go about binding my string complete with hyperlink tags into the TextBlock?

    Wednesday, April 23, 2014 3:02 PM
  • This can't be directly bound. You'll need to create a the Hyperlink object separately. The TextBlock doesn't read any embedded mark up.

    You can create an attached property which will read the mark up and create the inline objects for you. This will be similar to what I discussed in http://blogs.msdn.com/b/wsdevsol/archive/2013/09/26/binding-html-to-a-webview-with-attached-properties.aspx .

    Wednesday, April 23, 2014 3:20 PM
    Owner
  • Sorry I think I'm starting to understand, but isn't the method work for a WebView because you can make use of the WebView.NavigateToString() method?

    Whereas if I tried this, transforming the text into a sequence of Text and Hyperlink objects how would I then append these as the child elements of the TextBlock

    Wednesday, April 23, 2014 6:54 PM
  • Yes. For the WebView it will use NavigateToString. For other controls you'll change the OnXXXChanged() method to match. For a TextBlock we can set the Inlines property to the Inline objects (i.e. Runs and Hyperlinks) that we want.

    Here's a quick shot at using a Regexp to find the URLs and format the string as Xaml, parse the Xaml, and set the generated visual tree into the TextBlock. There may be a simpler way to parse this into Inlines, and I make no guarantees that regexp actually matches what you want. This will probably die horribly if you pass in actual XML tags: if you don't control the input make sure to quote it before parsing.

            private static void OnUrlTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                TextBlock tb = d as TextBlock;
                if (tb != null)
                {
                    string originalString = e.NewValue as string;
                    string urlPattern = @"(http(s)?://([\w+?\.\w+])+([a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)";
    
                    const string prelude = "<Paragraph xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'><Run>";
                    const string postscript = "</Run></Paragraph>";
                    const string openHyper = "</Run><Hyperlink NavigateUri=\"";
                    const string closeHyper = "</Hyperlink><Run>";
                    
                    Regex urlRegx = new Regex(urlPattern,RegexOptions.IgnoreCase);
                    MatchCollection matches = urlRegx.Matches(originalString);
                    
                    foreach (Match match in matches)
                    {
                        originalString = originalString.Replace(match.Value,
                            openHyper + match.Value + "\">" + match.Value + closeHyper);
                    }
    
                    string xaml = prelude + originalString + postscript;
                    Paragraph para = XamlReader.Load(xaml) as Paragraph;
                  
                    Inline[] inlines = new Inline[para.Inlines.Count];
                    para.Inlines.CopyTo(inlines, 0);
                    tb.Inlines.Clear();
                    para.Inlines.Clear();
                    foreach (var inline in inlines)
                    {
                       tb.Inlines.Add(inline);
                    }
                }
            }
    --Rob



    Wednesday, April 23, 2014 9:40 PM
    Owner
  • Thanks for the extended help, really appreciated!
    Thursday, April 24, 2014 11:22 AM