Forum Xamarin.Forms
We are excited to announce that the Xamarin Forums are moving to the new Microsoft Q&A experience. Q&A is the home for technical questions and answers at across all products at Microsoft now including Xamarin!

We encourage you to head over to Microsoft Q&A for .NET for posting new questions and get involved today.

Make multiple links in labels clickable

Apk07Apk07 Member ✭✭

I have labels with paragraphs of text that may or may not contain one or more URLs at any given time.
I want to parse the string of text, see a URL exists, and make it clickable to open in the default browser.

The trick is I'm trying to do this without using any controls that render it as HTML. I am hoping to use labels with spans or something of that sort. Most of the "HtmlLabel" packages and things of that sort technically work but render the label as HTML. For reasons too complicated to demonstrate, I need to avoid this.

Is there such a method or converter already out there?

Best Answer

Answers

  • Amar_BaitAmar_Bait DZMember ✭✭✭✭✭

    You may use Label.FormattedText with Spans.

  • Apk07Apk07 Member ✭✭

    @Amar_Bait said:
    You may use Label.FormattedText with Spans.

    I understand these exist, but my question is how to take a single string/label and have it automatically parse the rest for links and generate these spans. The links may not always be in the same place, there may be multiple at once, and I'm just databinding a single label

  • Amar_BaitAmar_Bait DZMember ✭✭✭✭✭
    edited July 2019

    Regex (https://stackoverflow.com/questions/6313033/extract-links-regex-c-sharp) or HtmlAgilityPack (https://stackoverflow.com/questions/2248411/get-all-links-on-html-page)

    Spans support DataBinding and GestureRecognizers so you can have a command that executes on tapping.

  • Amar_BaitAmar_Bait DZMember ✭✭✭✭✭
    edited July 2019

    Made a quick control using basic C# split on spaces

    public class LinksLabel : ContentView
        {
            public static BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(LinksLabel), propertyChanged: OnTextPropertyChanged);
    
            private readonly Label _label;
    
            private readonly ICommand _linkTapGesture = new Command<string>((url) => Device.OpenUri(new Uri(url)));
    
            public LinksLabel()
            {
                Content = _label = new Label();
            }
    
            public string Text
            {
                get => GetValue(TextProperty) as string;
                set => SetValue(TextProperty, value);
            }
    
            private void SetFormattedText()
            {
                var formattedString = new FormattedString();
    
               if (!string.IsNullOrEmpty(Text))
                {
                    var splitText = Text.Split(' ');
    
                    foreach (string textPart in splitText)
                    {
                        var span = new Span { Text = $"{textPart} " };
    
                        if (IsUrl(textPart)) // a link
                        {
                            span.TextColor = Color.DeepSkyBlue;
                            span.GestureRecognizers.Add(new TapGestureRecognizer
                            {
                                Command = _linkTapGesture,
                                CommandParameter = textPart
                            });
                        }
    
                        formattedString.Spans.Add(span);
                    }
                }
    
                _label.FormattedText = formattedString;
            }
    
            private bool IsUrl(string input)
            {
                return Uri.TryCreate(input, UriKind.Absolute, out var uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
            }
    
            private static void OnTextPropertyChanged(BindableObject bindable, object oldValue, object newValue)
            {
                var linksLabel = bindable as LinksLabel;
                linksLabel.SetFormattedText();
            }
    
    
        }
    

    Usage:

    <custom:LinksLabel Text="This Xamarin.Forms Forums question can be visited at https://forums.xamarin.com/discussion/161082/make-multiple-links-in-labels-clickable#latest but Google only at https://www.google.com/" />
    

    Result:

  • Apk07Apk07 Member ✭✭

    Thank you for the code! It seems to work well... I'm just a little new to custom controls so I'm wondering how I can make the control accept other standard Label attributes like HorizontalTextAlignment?

  • Apk07Apk07 Member ✭✭

    @Amar_Bait said:
    Do the same as I did for the control's Text property, create a static BindableProperty called HorizontalTextAlignmentProperty and a backing HorizontalTextAlignment property, then in the property changed method set the label's HorizontalTextAlignment property.

    If you want to support all Label properties, then it's easier to make the custom control inherits from a Label than implementing all Label properties one by one:

    public class LinksLabel : Label
        {
            public static BindableProperty LinksTextProperty = BindableProperty.Create(nameof(LinksText), typeof(string), typeof(LinksLabel), propertyChanged: OnLinksTextPropertyChanged);
    
            private readonly ICommand _linkTapGesture = new Command<string>((url) => Device.OpenUri(new Uri(url)));
    
            public string LinksText
            {
                get => GetValue(LinksTextProperty) as string;
                set => SetValue(LinksTextProperty, value);
            }
    
            private void SetFormattedText()
            {
                var formattedString = new FormattedString();
    
               if (!string.IsNullOrEmpty(LinksText))
                {
                    var splitText = LinksText.Split(' ');
    
                    foreach (string textPart in splitText)
                    {
                        var span = new Span { Text = $"{textPart} " };
    
                        if (IsUrl(textPart)) // a link
                        {
                            span.TextColor = Color.DeepSkyBlue;
                            span.GestureRecognizers.Add(new TapGestureRecognizer
                            {
                                Command = _linkTapGesture,
                                CommandParameter = textPart
                            });
                        }
    
                        formattedString.Spans.Add(span);
                    }
                }
    
                this.FormattedText = formattedString;
            }
    
            private bool IsUrl(string input)
            {
                return Uri.TryCreate(input, UriKind.Absolute, out var uriResult) &&
                (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
            }
    
            private static void OnLinksTextPropertyChanged(BindableObject bindable, object oldValue, object newValue)
            {
                var linksLabel = bindable as LinksLabel;
                linksLabel.SetFormattedText();
            }
        }
    

    Usage:

    <custom:LinksLabel 
      HorizontalTextAlignment = "Center"
      VerticalTextAlignment = "Top"
      TextColor = "DarkGray"
      LinksText = "This Xamarin.Forms Forums question can be visited at https://forums.xamarin.com/discussion/161082/make-multiple-links-in-labels-clickable#latest but Google only at https://www.google.com/" />
    

    Hey I know its been a while but I'm wondering if you might be able to offer help with this chunk of code you shared with me a while back...
    I have been using this for a while but I noticed if there are line-breaks directly after a URL, they will end up included in the URL because the code only Splits() on spaces. I made the code split on all whitespace by using Split(null) but then when it re-assembles the Spans, it effectively removes the linebreaks from my original string.
    I believe this is because of:

    var span = new Span { Text = $"{textPart} " };
    

    It always puts the parts back together with a space at the end, because it was only expecting to split on spaces. How can I make it both split on a line-break and retain the linebreak if it had a linebreak to begin with?

Sign In or Register to comment.