locked
Make multiple links in labels clickable RRS feed

  • Question

  • User386163 posted

    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?

    Saturday, July 6, 2019 7:30 AM

Answers

  • User74518 posted

    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/" />

    • Marked as answer by Anonymous Thursday, June 3, 2021 12:00 AM
    Tuesday, July 9, 2019 12:49 AM

All replies

  • User74518 posted

    You may use Label.FormattedText with Spans.

    Saturday, July 6, 2019 11:24 AM
  • User386163 posted

    @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

    Saturday, July 6, 2019 3:53 PM
  • User74518 posted

    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.

    Saturday, July 6, 2019 3:56 PM
  • User74518 posted

    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:

    Saturday, July 6, 2019 5:27 PM
  • User386163 posted

    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?

    Monday, July 8, 2019 11:03 PM
  • User74518 posted

    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/" />

    • Marked as answer by Anonymous Thursday, June 3, 2021 12:00 AM
    Tuesday, July 9, 2019 12:49 AM
  • User386163 posted

    @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?

    Thursday, November 21, 2019 9:13 PM