I needed a simple control that could show a hyperlink inline with other text and I’d like the link to be defined in markdown.
In short, something like this:
HyperlinkLabel
In Xamarin.Forms, I created a HyperlinkLabel class which isn’t anything more than a class inheriting from the standard Label with an additional bindable property RawText. It also has a method GetText that parses the RawText property using a RegexExpression. The method returns the text that eventually will get displayed (without the markdown). Through the out parameter, this method returns all info we need about all the links in the text: the text of the link, the url and the index where this link is positioned in the Text.
public class HyperlinkLabel : Label { public string RawText { get => (string)GetValue(RawTextProperty); set => SetValue(RawTextProperty, value); } public static readonly BindableProperty RawTextProperty = BindableProperty.Create(nameof(RawText), typeof(string), typeof(HyperlinkLabel), null); public string GetText(out List<HyperlinkLabelLink> links) { links = new List<HyperlinkLabelLink>(); if (RawText == null) return null; string pattern = @"\[([^]]*)\]\(([^\s^\)]*)\)"; var linksInText = Regex.Matches(RawText, pattern, RegexOptions.IgnoreCase); string Text = RawText; for (int i = 0; i < linksInText.Count; i++) { string fullMatch = linksInText[i].Groups[0].Value; string text = linksInText[i].Groups[1].Value; string link = linksInText[i].Groups[2].Value; int start = Text.IndexOf(fullMatch); if (start > -1) { Text = $"{Text.Substring(0, start)}{text}{Text.Substring(start + fullMatch.Length)}"; links.Add(new HyperlinkLabelLink(text, link, start)); } } return Text; } }
public class HyperlinkLabelLink { internal HyperlinkLabelLink(string text, string link, int start) { Text = text; Link = link; Start = start; } public string Text { get; } public string Link { get; } public int Start { get; } }
iOS
In order to get this in iOS, we both need a custom control and a Renderer.
HyperlinkUIView
A UITextView in iOS is able to render inline links, through the usage of a NSAttributedString. A NSAttributedString is a string with attributes… (Who would have thought that :-)). These attributes can define, for a particular range, all sorts of stuff like the font, color, … and whether a piece of text is a link. But a UITextView also has some standard behavior that I don’t want like the text being selectable. That’s why I created a custom control HyperlinkUIView that inherits from UITextView and overrides these behaviors. It also automatically sets its size to the size of the text.
public class HyperlinkUIView : UITextView { public HyperlinkUIView() { Selectable = true; Editable = false; BackgroundColor = UIColor.Clear; } public override bool CanBecomeFirstResponder => false; public override bool GestureRecognizerShouldBegin(UIGestureRecognizer gestureRecognizer) { //Preventing standard actions on UITextView that are triggered after long press if (gestureRecognizer is UILongPressGestureRecognizer longpress && longpress.MinimumPressDuration == .5) return false; return true; } public override bool CanPerform(Selector action, NSObject withSender) => false; public override void LayoutSubviews() { //Make the TextView as large as its content base.LayoutSubviews(); var x = new CGSize(this.Frame.Size.Width, double.MaxValue); var fits = SizeThatFits(x); var frame = Frame; frame.Size = fits; } }
HyperlinkLabelRenderer
This renderer on iOS will ‘translate’ the HyperlinkLabel to the HyperlinkUIView. There isn’t anything special going on in this class. In the SetText method, we ask the Text that should be printed from the HyperlinkLabel. We loop over the links that we also get back from the HyperlinkLabel’s GetText method and we simple add the necessary attributes to the NSMutableAttributedString.
public class HyperlinkLabelRenderer : ViewRenderer<HyperlinkLabel, HyperlinkUIView> { protected override void OnElementChanged(ElementChangedEventArgs<HyperlinkLabel> e) { base.OnElementChanged(e); if (e.NewElement != e.OldElement) { if (e.OldElement != null) e.OldElement.PropertyChanged -= Element_PropertyChanged; if (e.NewElement != null) e.NewElement.PropertyChanged += Element_PropertyChanged; } var textView = new HyperlinkUIView(); SetNativeControl(textView); SetText(); } private void Element_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == HyperlinkLabel.RawTextProperty.PropertyName) SetText(); } private void SetText() { CTStringAttributes attrs = new CTStringAttributes(); string text = Element.GetText(out List<HyperlinkLabelLink> links); if (text != null) { var str = new NSMutableAttributedString(text); str.AddAttribute(UIStringAttributeKey.Font, Element.Font.ToUIFont(), new NSRange(0, str.Length)); var textColor = (Color)Element.GetValue(Label.TextColorProperty); str.AddAttribute(UIStringAttributeKey.ForegroundColor, textColor.ToUIColor(Color.Black), new NSRange(0, str.Length)); foreach (var item in links) { str.AddAttribute(UIStringAttributeKey.Link, new NSUrl(item.Link), new NSRange(item.Start, item.Text.Length)); } Control.AttributedText = str; } } }
Please mind that I use Element.Font.ToUIFont() to get the UIFont from the Element. However, the Font property is deprecated, but for now it still works. As far as I know, there is no other way to easily translate the font attributes defined on a Xamarin Forms control to a UIFont. There is, however, an extension method in Xamarin Forms that does this: internal static UIFont ToUIFont(this Label label). But, as you can see, this method is internal, so we can’t use it in our own code and I didn’t feel like copy en pasting this code. Xamarin should just make this method public, right? 😉
UWP
There is no need for a custom control in UWP as a TextBlock already supports inline links. So the only thing we need is a custom renderer.
HyperlinkLabelRenderer
This class does more or less the same thing as its counterpart in iOS. But instead of working with attributed strings, we can put Runs and Hyperlinks inside a TextBlock. So we are basically going to chop our Text into pieces (‘pure’ text or links) and add them as a Run or Hyperlink respectively to the Inlines collection of our TextBlock.
public class HyperlinkLabelRenderer : ViewRenderer<HyperlinkLabel, TextBlock> { protected override void OnElementChanged(ElementChangedEventArgs<HyperlinkLabel> e) { base.OnElementChanged(e); if (e.NewElement != e.OldElement) { if (e.OldElement != null) e.OldElement.PropertyChanged -= Element_PropertyChanged; if (e.NewElement != null) e.NewElement.PropertyChanged += Element_PropertyChanged; } var tb = new TextBlock(); tb.TextWrapping = Windows.UI.Xaml.TextWrapping.WrapWholeWords; SetNativeControl(tb); SetText(); } private void SetText() { Control.Inlines.Clear(); string text = Element.GetText(out List<HyperlinkLabelLink> links); if (text != null) { int index = 0; foreach (var item in links) { Control.Inlines.Add(new Run() { Text = text.Substring(index, item.Start - index) }); var hl = new Hyperlink(); hl.NavigateUri = new Uri(item.Link); hl.Inlines.Add(new Run() { Text = text.Substring(item.Start, item.Text.Length) }); Control.Inlines.Add(hl); index = item.Start + item.Text.Length; if(index < text.Length && item == links.LastOrDefault()) Control.Inlines.Add(new Run() { Text = text.Substring(index, text.Length - index) }); } } } private void Element_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == HyperlinkLabel.RawTextProperty.PropertyName) SetText(); } }
Android
Update 7 December 2018: A year later, I finally made an Android renderer for this control. More info can be found in this post.
Remarks
This is just a very basic implementation for iOS and UWP, I don’t have an Android version yet. Also, I don’t take every property that has been set on the Label into account (fontsize, color, … on UWP and other things as well). But please, if you need this stuff and feel like implementing it, my code is on GitHub and just do a PR! 🙂
Find the code on GitHub.
Remarks, questions? Just drop a comment and I’ll get back to you.
May 11, 2018 at 7:47 pm
do you have android version of this? it looks promising.
May 14, 2018 at 11:22 am
No I haven’t unfortunately. Didn’t find the time yet to implement this on Android.
May 15, 2018 at 11:02 am
Can you please add the android code base for this?Much appreciated !
May 15, 2018 at 12:50 pm
I honestly don’t have the time for this at the moment. I can’t promise you anything, but I’ll try and look to implement this as soon as I find the chance.
December 7, 2018 at 7:48 pm
Android implementation can be found here: https://blog.pieeatingninjas.be/2018/12/07/using-android-linkify-to-add-inline-links-to-a-xamarin-forms-label/
April 30, 2022 at 9:08 am
Thank you for this functionality, loving it. How would you go about getting an event when a link is clicked? also maybe having the clicked section as a parameter of that event