coyotesword
coyotesword

Reputation: 1

How do I select and perform actions upon selected text in a .NET MAUI Editor?

I'm building a word processor with the .NET MAUI framework. I would like to be able to a) detect when a user has selected text, and what that selected text is, within the Editor control, and b) retrieve the selected text such that I can implement formatting and clipboard functionalities.

I already understand how to setup and use the basics of the Editor. I've only gotten so far as to find out that I need to use a renderer; however, even this is unreliable, as all available resources are from the last .NET MAUI update, so every "solution" is deprecated. I'm also designing this primarily for Windows, but of course it would be best if I could do this on any given platform.

Has anyone implemented similar functionality in a .NET MAUI app, and if so, how did you handle text selection?

Upvotes: 0

Views: 957

Answers (2)

BIT
BIT

Reputation: 114

The solution with CursorPosition and SelectionLength is pretty good due to it's simplicity, but if you want to handle it with MAUI Handlers here is how you can do it.

Create Model to share data from native controls:

public class EntryTextSelection
{
    // Represents selected text, but you can add other properties if needed
    public string Text { get; set; } = string.Empty;
}

Create custom Entry and define BindableProperty to provide Command on selection change:

public class TextSelectedEntry : Entry
{
    public static readonly BindableProperty SelectionChangedCommandProperty = BindableProperty.Create(nameof(SelectionChangedCommand), typeof(Command<EntryTextSelection>), typeof(TextSelectedEntry), null, BindingMode.OneTime);

    public Command<EntryTextSelection> SelectionChangedCommand
    {
        get => (Command<EntryTextSelection>)GetValue(SelectionChangedCommandProperty);
        set => SetValue(SelectionChangedCommandProperty, value);
    }
}

Create core partial EntryHandler, which will have per platform implementation:

// Make sure all per platform implementation, will be under the same namespace
namespace TabsDataShare.Handlers
{
    public partial class TextSelectedEntryHandler : EntryHandler
    {
    }
}

Register Handler in MauiProgram.cs:

builder
    .ConfigureMauiHandlers(handlers => {
        handlers.AddHandler<TextSelectedEntry, TextSelectedEntryHandler>();
    });

Windows Handler

Just subscribe to SelectionChanged event and execute Command on event trigger:

namespace TabsDataShare.Handlers
{
    public partial class TextSelectedEntryHandler
    {
        protected override void ConnectHandler(TextBox platformView)
        {
            base.ConnectHandler(platformView);

            platformView.SelectionChanged += PlatformView_SelectionChanged;
        }

        protected override void DisconnectHandler(TextBox platformView)
        {
            platformView.SelectionChanged -= PlatformView_SelectionChanged;

            base.DisconnectHandler(platformView);
        }

        private void PlatformView_SelectionChanged(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
        {
            TextBox textBox = (TextBox)sender;
            TextSelectedEntry? textEntry = VirtualView as TextSelectedEntry;
            if (textEntry?.SelectionChangedCommand != null)
            {
                EntryTextSelection model = new()
                {
                    Text = textBox.SelectedText
                };
                textEntry.SelectionChangedCommand.Execute(model);
            }
        }
    }
}

Android Handler

For Android we need to override OnSelectionChanged of AppCompatEditText. So we can implement control based on AppCompatEditText:

public class AppCompatSelectionEditText : AppCompatEditText
{
    public Command<EntryTextSelection>? OnSelectionCommand { get; set; }

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

    protected override void OnSelectionChanged(int selStart, int selEnd)
    {
        base.OnSelectionChanged(selStart, selEnd);

        if (OnSelectionCommand != null)
        {
            EntryTextSelection selection = new EntryTextSelection
            {
                Text = Text?[selStart..selEnd] ?? string.Empty
            };
            OnSelectionCommand.Execute(selection);
        }
    }
}

After that we can define Handler for Android:

namespace TabsDataShare.Handlers
{
    public partial class TextSelectedEntryHandler
    {
        public TextSelectedEntryHandler() : base() 
        {
            Mapper.Add(nameof(TextSelectedEntry.SelectionChangedCommand), MapSelectionChangedCommand);
        }

        protected override AppCompatEditText CreatePlatformView()
        {
            TextSelectedEntry? textEntry = VirtualView as TextSelectedEntry;
            // Here we initialize our custom EditText and assign Command
            var nativeEntry = new AppCompatSelectionEditText(Context);
            if (textEntry != null)
            {
                nativeEntry.OnSelectionCommand = textEntry.SelectionChangedCommand;
            }

            return nativeEntry;
        }

        public static void MapSelectionChangedCommand(IEntryHandler handler, IEntry entry)
        {
            AppCompatSelectionEditText? nativeEntry = handler.PlatformView as AppCompatSelectionEditText;
            TextSelectedEntry? textEntry = entry as TextSelectedEntry;
            if (nativeEntry != null && textEntry != null) 
            {
                nativeEntry.OnSelectionCommand = textEntry.SelectionChangedCommand;
            }
        }
    }
}

iOS Handler

I haven't found better solution, so it follows the same approach as Android. First create your TextField control:

public class SelectionTextField : MauiTextField
{
    public Command<EntryTextSelection>? OnSelectionCommand { get; set; }

    public override UITextRange? SelectedTextRange
    { 
        get => base.SelectedTextRange;
        set
        {
            var old = base.SelectedTextRange;

            base.SelectedTextRange = value;

            if (value != null && OnSelectionCommand != null && (old?.Start != value?.Start || old?.End != value?.End))
            {
                EntryTextSelection selection = new EntryTextSelection
                {
                     Text = this.TextInRange(value)
                };
                OnSelectionCommand.Execute(selection);
            }
        }
    }
}

Then create Handler:

namespace TabsDataShare.Handlers
{
    public partial class TextSelectedEntryHandler
    {
        public TextSelectedEntryHandler() : base()
        {
            Mapper.Add(nameof(TextSelectedEntry.SelectionChangedCommand), MapSelectionChangedCommand);
        }

        protected override MauiTextField CreatePlatformView()
        {
            TextSelectedEntry? textEntry = VirtualView as TextSelectedEntry;
            
            // Initialize our TextField and assign Command
            var textField = new SelectionTextField
            {
                BorderStyle = UITextBorderStyle.RoundedRect,
                ClipsToBounds = true
            };
            if (textEntry != null)
            {
                textField.OnSelectionCommand = textEntry.SelectionChangedCommand;
            }

            return textField;
        }

        public static void MapSelectionChangedCommand(IEntryHandler handler, IEntry entry)
        {
            SelectionTextField? nativeEntry = handler.PlatformView as SelectionTextField;
            TextSelectedEntry? textEntry = entry as TextSelectedEntry;
            if (nativeEntry != null && textEntry != null)
            {
                nativeEntry.OnSelectionCommand = textEntry.SelectionChangedCommand;
            }
        }
    }
}

Upvotes: 0

Stephen Quan
Stephen Quan

Reputation: 25871

To get things started, I recommend you look at different Binding to the CursorPosition and SelectionLength properties, for example:

<VerticalStackLayout>
    <Editor x:Name="editor" Text="Hello World" />
    <Label Text="{Binding CursorPosition, Source={x:Reference editor}}"/>
    <Label Text="{Binding SelectionLength, Source={x:Reference editor}}"/>
</VerticalStackLayout>

You should see that every cursor moves and every selection will update both properties accordingly. You can see that this forms a basis for retrieving the selection text.

The next thing you want to do is put CursorPosition and SelectionLength in your view model, we can push those values from the Editor with a OneWayToSource Binding:

<VerticalStackLayout>
    <Editor x:Name="editor"
            CursorPosition="{Binding CursorPosition, Mode=OneWayToSource}"
            SelectionLength="{Binding SelectionLength, Mode=OneWayToSource}"
            Text="{Binding Text, Mode=TwoWay}"/>
    <Label Text="{Binding CursorPosition}"/>
    <Label Text="{Binding SelectionLength}"/>
    <Label Text="{Binding SelectedText}"/>
</VerticalStackLayout>

Here's the view model for the above:

    private int _cursorPosition = 0;
    public int CursorPosition
    {
        get => _cursorPosition;
        set
        {
            _cursorPosition = value;
            OnPropertyChanged(nameof(CursorPosition));
            OnPropertyChanged(nameof(SelectedText));
        }
    }

    private int _selectionLength = 0;
    public int SelectionLength
    {
        get => _selectionLength;
        set
        {
            _selectionLength = value;
            OnPropertyChanged(nameof(SelectionLength));
            OnPropertyChanged(nameof(SelectedText));
        }
    }

    private string _text = "Hello World";
    public string Text
    {
        get => _text;
        set
        {
            _text = value;
            OnPropertyChanged(nameof(Text));
            OnPropertyChanged(nameof(SelectedText));
        }
    }

    public string SelectedText
    {
        get
        {
            if (Text == null || Text == string.Empty)
                return string.Empty;
            if (SelectionLength <= 0)
                return string.Empty;
            if (CursorPosition + SelectionLength > Text.Length)
                return string.Empty;
            return Text.Substring(CursorPosition, SelectionLength);
        }
    }

From this point onwards, it's up to you what to do with this information.

Upvotes: 3

Related Questions