Selectable text with no context menu in Xamarin Forms
Tags: xamarin, xamarinforms, ios, android
Published: 2021-04-24 14:55:11

Recently I've been working on an app where users can read articles in a foreign language and get an immediate translation of the words they select. Unfortunately, it turns out that text selection isn't supported out of the box in Xamarin Forms so I needed to do some digging.

First of all, I found these two great articles explaining how to create selectable labels:

\- [Article by @HeikkiDev](https://medium.com/@HeikkiDev/selectable-label-on-xamarin-forms-9b050267bf8e)

\- [Article by @anna.domashych](https://medium.com/@anna.domashych/selectable-read-only-multiline-text-field-in-xamarin-forms-69d09276d580)

That's almost what I needed but the default selection functionality shows a context menu allowing the user to copy text etc. In my application, I wanted to handle the selected word on my own, without showing the menu. Here is how to do it.



### Shared code

First of all, we need to define a custom control in our shared project. It will support basic text properties and a bindable command to get the currently selected word.

```
public class SelectableLabel : View
    {
        public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(SelectableLabel), default(string));
        public static readonly BindableProperty TextColorProperty = BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(SelectableLabel), Color.Black);
        public static readonly BindableProperty FontAttributesProperty = BindableProperty.Create(nameof(FontAttributes), typeof(FontAttributes), typeof(SelectableLabel), FontAttributes.None);
        public static readonly BindableProperty FontSizeProperty = BindableProperty.Create(nameof(FontSize), typeof(double), typeof(SelectableLabel), -1.0);

        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        public Color TextColor
        {
            get { return (Color)GetValue(TextColorProperty); }
            set { SetValue(TextColorProperty, value); }
        }

        public FontAttributes FontAttributes
        {
            get { return (FontAttributes)GetValue(FontAttributesProperty); }
            set { SetValue(FontAttributesProperty, value); }
        }

        [TypeConverter(typeof(FontSizeConverter))]
        public double FontSize
        {
            get { return (double)GetValue(FontSizeProperty); }
            set { SetValue(FontSizeProperty, value); }
        }

        public static readonly BindableProperty OnTextSelectedCommandProperty =
            BindableProperty.Create(nameof(OnTextSelectedCommand), typeof(ICommand), typeof(SelectableLabel), null);

        public ICommand OnTextSelectedCommand
        {
            get { return (ICommand)GetValue(OnTextSelectedCommandProperty); }
            set { SetValue(OnTextSelectedCommandProperty, value); }
        }
      
        public static void Execute(ICommand command, object parameter)
        {
            if (command == null) return;
            if (command.CanExecute(parameter))
            {
                command.Execute(parameter);
            }
        }

        public Command<string> OnTextSelected => new Command<string>((s) => Execute(OnTextSelectedCommand, s));
    }
```

 

### iOS 

On iOS, we enable selection by setting UITextView's properties `Editable = false` and `Selectable = true`. In order to remove the context menu, we have to subclass UITextView and override its `CanPerform(Selector action, NSObject withSender)` function to return **false**. I also added a callback to send the selected word back to the shared code.

```
[assembly: ExportRenderer(typeof(SelectableLabel), typeof(XFSelectableLabel.iOS.Renderers.SelectableLabelRenderer))]
namespace XFSelectableLabel.iOS.Renderers
{
    public class SelectableLabelRenderer : ViewRenderer<SelectableLabel, UITextView>
    {
        CustomTextView uiTextView;

        protected override void OnElementChanged(ElementChangedEventArgs<SelectableLabel> e)
        {
            base.OnElementChanged(e);

            var label = (SelectableLabel)Element;
            if (label == null)
                return;

            if (Control == null)
            {
                uiTextView = new CustomTextView(Element.OnTextSelected.Execute);
            }

            uiTextView.Selectable = true;
            uiTextView.Editable = false;
            uiTextView.ScrollEnabled = false;
            uiTextView.TextContainerInset = UIEdgeInsets.Zero;
            uiTextView.TextContainer.LineFragmentPadding = 0;
            uiTextView.BackgroundColor = UIColor.Clear;

            // Initial properties Set
            uiTextView.Text = label.Text;
            uiTextView.TextColor = label.TextColor.ToUIColor();
            switch (label.FontAttributes)
            {
                case FontAttributes.None:
                    uiTextView.Font = UIFont.SystemFontOfSize(new nfloat(label.FontSize));
                    break;
                case FontAttributes.Bold:
                    uiTextView.Font = UIFont.BoldSystemFontOfSize(new nfloat(label.FontSize));
                    break;
                case FontAttributes.Italic:
                    uiTextView.Font = UIFont.ItalicSystemFontOfSize(new nfloat(label.FontSize));
                    break;
                default:
                    uiTextView.Font = UIFont.BoldSystemFontOfSize(new nfloat(label.FontSize));
                    break;
            }

            SetNativeControl(uiTextView);
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == SelectableLabel.TextProperty.PropertyName)
            {
                if (Control != null && Element != null && !string.IsNullOrWhiteSpace(Element.Text))
                {
                    uiTextView.Text = Element.Text;
                }
            }
            else if (e.PropertyName == SelectableLabel.TextColorProperty.PropertyName)
            {
                if (Control != null && Element != null)
                {
                    uiTextView.TextColor = Element.TextColor.ToUIColor();
                }
            }
            else if (e.PropertyName == SelectableLabel.FontAttributesProperty.PropertyName
                        || e.PropertyName == SelectableLabel.FontSizeProperty.PropertyName)
            {
                if (Control != null && Element != null)
                {
                    switch (Element.FontAttributes)
                    {
                        case FontAttributes.None:
                            uiTextView.Font = UIFont.SystemFontOfSize(new nfloat(Element.FontSize));
                            break;
                        case FontAttributes.Bold:
                            uiTextView.Font = UIFont.BoldSystemFontOfSize(new nfloat(Element.FontSize));
                            break;
                        case FontAttributes.Italic:
                            uiTextView.Font = UIFont.ItalicSystemFontOfSize(new nfloat(Element.FontSize));
                            break;
                        default:
                            uiTextView.Font = UIFont.BoldSystemFontOfSize(new nfloat(Element.FontSize));
                            break;
                    }
                }
            }
        }

        public class CustomTextView : UITextView
        {
            private Action<string> _callback;

            public CustomTextView(Action<string> callback)
            {
                _callback = callback;
            }

            public override bool CanPerform(Selector action, NSObject withSender)
            {
                var word = Text.Substring((int)SelectedRange.Location, (int)SelectedRange.Length);

                _callback(word);

                return false;
            }
        }
    }
```



### Android

To make Android TextView selectable we need to call `textView.SetTextIsSelectable(true)`. This should be enough, however there might be an issue of it not working where you will see an error in the console log saying "*TextView does not support text selection. Selection cancelled.*" This seems to be an Android bug, which you can work around by overriding `OnAttachedToWindow()` function in your renderer code like so:

```
protected override void OnAttachedToWindow()
{
    base.OnAttachedToWindow();
    textView.Enabled = false;
    textView.Enabled = true;
}
```

In order to remove the context menu, we need to create our own `CustomSelectionActionModeCallback` and overwrite its `OnActionItemClicked `and `OnPrepareActionMode` functions. The whole renderer code looks like this:

```
[assembly: ExportRenderer(typeof(SelectableLabel), typeof(XFSelectableLabel.Droid.Renderers.SelectableLabelRenderer))]

namespace XFSelectableLabel.Droid.Renderers
{
    public class SelectableLabelRenderer : ViewRenderer<SelectableLabel, TextView>
    {
        TextView textView;

        public SelectableLabelRenderer(Context context) : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<SelectableLabel> e)
        {
            base.OnElementChanged(e);

            var label = (SelectableLabel)Element;

            if (label == null) return;

            if (Control == null)
            {
                textView = new TextView(this.Context);
            }

            textView.Enabled = true;
            textView.SetTextIsSelectable(true);
            textView.DefaultFocusHighlightEnabled = true;

            textView.CustomSelectionActionModeCallback = new CustomSelectionActionModeCallback(TextSelected);

            textView.Text = label.Text;

            textView.SetTextColor(label.TextColor.ToAndroid());

            switch (label.FontAttributes)
            {
                case FontAttributes.None:
                    textView.SetTypeface(null, Android.Graphics.TypefaceStyle.Normal);
                    break;
                case FontAttributes.Bold:
                    textView.SetTypeface(null, Android.Graphics.TypefaceStyle.Bold);
                    break;
                case FontAttributes.Italic:
                    textView.SetTypeface(null, Android.Graphics.TypefaceStyle.Italic);
                    break;
                default:
                    textView.SetTypeface(null, Android.Graphics.TypefaceStyle.Normal);
                    break;
            }

            textView.TextSize = (float)label.FontSize;
            SetNativeControl(textView);
        }

        private void TextSelected()
        {
            var word = textView.Text[textView.SelectionStart..textView.SelectionEnd];

            Element.OnTextSelected.Execute(word);
        }

        protected override void OnAttachedToWindow()
        {
            base.OnAttachedToWindow();
            textView.Enabled = false;
            textView.Enabled = true;
        }

        private class CustomSelectionActionModeCallback : Java.Lang.Object, ActionMode.ICallback
        {
            private Action _selectionCallback;

            public CustomSelectionActionModeCallback(Action selectionCallback)
            {
                _selectionCallback = selectionCallback;
            }

            public bool OnActionItemClicked(ActionMode m, IMenuItem i) => false;

            public bool OnCreateActionMode(ActionMode mode, IMenu menu) => true;

            public void OnDestroyActionMode(ActionMode mode) { }

            public bool OnPrepareActionMode(ActionMode mode, IMenu menu)
            {
                menu?.Clear();
                _selectionCallback?.Invoke();

                return false;
            }
        }
    }
}

```



That's it! All that's left now is to add the label into your view and bind its `OnTextSelectedCommand` to get the selected word. An example of how to do it and the whole code is available here:

<https://github.com/KlaudiaBronowicka/XFSelectableLabel>



Happy coding!