0

I am trying to make a custom renderer for an editor that changes the "return" key to a "done" button and fires the Completed event when you tap it instead of typing a newline. The code in OnElementChanged() is being hit, but it's not doing anything. The "return" key is still a "return" key and it still types newlines instead of making the editor go out of focus. What am I doing wrong?

Here is the class for the custom editor (in the .NET project):

using Xamarin.Forms;

namespace Partylist.Custom_Controls
{
    public class ChecklistEditor : Editor
    {

    }
}

Here is the custom renderer for Android:

using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Views.InputMethods;
using Android.Widget;

using Partylist.Custom_Controls;
using Partylist.Droid.Custom_Renderers;

using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(ChecklistEditor), typeof(ChecklistEditorRenderer))]
namespace Partylist.Droid.Custom_Renderers
{
    class ChecklistEditorRenderer : EditorRenderer
    {
        // Constructor because it needs to exist.
        public ChecklistEditorRenderer(Context context) : base(context)
        {

        }

        // This gets overridden so I can change what I want to change.
        protected override void OnElementChanged(ElementChangedEventArgs
            <Editor> e)
        {
            // Make it do what is should normally do so it will exist.
            base.OnElementChanged(e);
            // Make the "Return" button on the keyboard be a "Done" button.
            Control.ImeOptions = ImeAction.Done;
            // Make the thing watch for when the user hits the "Return" button.
            Control.EditorAction += OnEditorAction;
        }

        // This makes the "Return" button fire the "Completed" event 
        // instead of typing a newline.
        private void OnEditorAction(object sender, TextView
            .EditorActionEventArgs e)
        {
            e.Handled = false;
            if (e.ActionId == ImeAction.Done)
            {
                Control.ClearFocus();
                e.Handled = true;
            }
        }
    }
}

Here is the custom renderer for iOS:

using Foundation;
using Partylist.Custom_Controls;
using Partylist.iOS.Custom_Renderers;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(ChecklistEditor), typeof(ChecklistEditorRenderer))]
namespace Partylist.iOS.Custom_Renderers
{
    class ChecklistEditorRenderer : EditorRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs
            <Editor> e)
        {
            base.OnElementChanged(e);

            Control.ReturnKeyType = UIReturnKeyType.Done;
        }


        protected override bool ShouldChangeText(UITextView textView, 
            NSRange range, string text)
        {
            if (text == "\n")
            {
                textView.ResignFirstResponder();
                return false;
            }

            return true;
        }
    }
}

The code-behind for the page where I'm using these custom renderers (there's nothing in the XAML that should conflict with it, I think, but I'll add it to the post if people want to make sure):

using Partylist.Custom_Controls;

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace Partylist.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class ChecklistPage : ContentPage
    {
        // Struct for items on the checklist.
        struct Item
        {
            public ChecklistEditor ItemEditor { get; set; }
            public CheckBox ItemCheckbox { get; set; }
        }
        // Create a list of contact structs to populate the ListView.
        ObservableCollection<Item> items;
        // Flag for when an item is added to the list.
        bool itemAdded = false;

        // Constructor.
        public ChecklistPage()
        {
            // Whatever setup stuff it was going to do anyway.
            InitializeComponent();
            // Set the label's BindingContext to the 
            // App class so it can update its text.
            tipLabel.BindingContext = (App)App.Current;
        }

        // Override for OnAppearing().
        protected override void OnAppearing()
        {
            // Makes the page appear.
            base.OnAppearing();
            // Set the page's title to be the name of the selected list.
            Title = App.selectedList.Name;
            // Make a toolbar item appear to access the Main Checklist
            // unless we are already there.
            if (App.selectedList.ListFile.Name.EndsWith(".mchec"))
            {
                ToolbarItems.Remove(MainChecklistButton);
            }
            // Set the binding context of the page to itself.
            BindingContext = this;
            // Start the timer for the tips banner if it is stopped.
            App.tipTimer.Start();
            // Set the banner's text to the current tip's sumamry.
            tipLabel.Text = ((App)App.Current).CurrentTip.Summary;
            OnPropertyChanged("CurrentTip");
            // Subscribe the OnTipUpdate function to the tipUpdate event in the app
            // class.
            App.TipUpdate += OnTipUpdate;

            // Make the ObservableCOllection reference something.
            items = new ObservableCollection<Item>();
            // Open a stream to the list that we want to display.
            using (StreamReader listReader = new StreamReader(App.selectedList
                .ListFile.FullName))
            {
                // Loop through the file and read data into the list.
                while (!listReader.EndOfStream)
                {
                    // Create a blank item.
                    Item newItem = new Item()
                    {
                        ItemEditor = new ChecklistEditor()
                        {
                            Text = listReader.ReadLine(),
                            Placeholder = "New Item",
                            IsTabStop = true,
                            AutoSize = EditorAutoSizeOption.TextChanges,
                            WidthRequest = 310
                        },
                        ItemCheckbox = new CheckBox()
                        {
                            Color = App.selectedList.ListItemColor,
                            IsChecked = bool.Parse(listReader.ReadLine())
                        }
                    };
                    // Subscribe OnCompleted() to the new item's "Completed" 
                    // event.
                    newItem.ItemEditor.Completed += OnCompleted;
                    // Subscribe OnTextChanged() to the new item's 
                    // "TextChanged" event.
                    newItem.ItemEditor.TextChanged += OnTextChanged;
                    // Add the new item to the list.
                    items.Add(newItem);
                    // Make the ListView update.
                    ChecklistView.ItemsSource = items;
                    OnPropertyChanged("contacts");
                }
                // Once everything is loaded, close the file.
                listReader.Close();
            }
        }

        // Override for OnDisappearing().
        protected override void OnDisappearing()
        {
            // Makes the page disappear.
            base.OnDisappearing();
            // Open a stream to the file for the list.
            StreamWriter listWriter = new StreamWriter(App.selectedList
                .ListFile.FullName);
            // Loop through the contacts list to write the contacts to the 
            // file.
            for (int i = 0; i < items.Count; i++)
            {
                // Write each item to the file.
                listWriter.WriteLine(items.ElementAt(i).ItemEditor.Text);
                listWriter.WriteLine(items.ElementAt(i).ItemCheckbox.IsChecked);
            }
            // Close the stream.
            listWriter.Close();
        }

        // Function for when the "Add New Contact" button is clicked.
        private void OnAddNewItemClicked(object sender, EventArgs e)
        {
            // Create a blank item.
            Item newItem = new Item()
            {
                ItemEditor = new ChecklistEditor()
                {
                    Placeholder = "New Item",
                    IsTabStop = true,
                    AutoSize = EditorAutoSizeOption.TextChanges,
                    WidthRequest = 310
                },
                ItemCheckbox = new CheckBox()
                {
                    Color = App.selectedList.ListItemColor,
                    IsChecked = false
                }
            };
            // Subscribe OnCompleted() to the new item's "Completed" 
            // event.
            newItem.ItemEditor.Completed += OnCompleted;
            // Subscribe OnTextChanged() to the new item's 
            // "TextChanged" event.
            newItem.ItemEditor.TextChanged += OnTextChanged;
            // Add the new contact to the list.
            items.Add(newItem);
            // Set the "itemAdded" flag to true.
            itemAdded = true;
            // Make the ListView update.
            ChecklistView.ItemsSource = items;
            OnPropertyChanged("contacts");
            // Select the new item.
            ChecklistView.SelectedItem = items.ElementAt(items.Count - 1);
        }

        // Function for when an item is selected, used to set the focus to
        // a newly added item in the list.
        private async void OnItemSelected(object sender, SelectedItemChangedEventArgs e)
        {
            // Only runs this if an item was added (as opposed to being 
            // read in from the file).
            if (itemAdded)
            {
                if (e.SelectedItem == null) return;
                await Task.Delay(100); // Change the delay time if Focus() doesn't work.
                ((Item)e.SelectedItem).ItemEditor.Focus();
                ChecklistView.SelectedItem = null;
                itemAdded = false;
            }
        }

        // Function for when the user presses "Return" on the keyboard in
        // an editor.
        private void OnCompleted(object sender, EventArgs e)
        {
            // We just want to unfocus the editor.
            ((Editor)sender).Unfocus();
        }

        // Function for when the user types anything in the editor, used 
        // to make sure it resizes.
        private void OnTextChanged(object sender, TextChangedEventArgs e)
        {
            // Makes the cell resize. The cell is the parent of the 
            // StackLayout which is the parent of the ContentView which is
            // the parent of the Editor that fired the event.
            ((ViewCell)((Editor)sender).Parent.Parent.Parent)
                .ForceUpdateSize();
        }
    }
}
Collin Vesel
  • 61
  • 1
  • 1
  • 9
  • The latest Xamarin Forms package adds the ReturnType attribute for Entry elements. It will also execute a command when the Done button is clicked. The IMEAction types for Done, Next, Search, Go and Send are all supported now. – Anand Jul 06 '20 at 15:18
  • @Anand But entries can’t wrap text. – Collin Vesel Jul 06 '20 at 15:25

1 Answers1

1

In Android , you need to set Single Line for EditTextView , then it will works .

For example :

...
    protected override void OnElementChanged(ElementChangedEventArgs<Editor> e)
    {
        base.OnElementChanged(e);
        // set single line will works
        Control.SetSingleLine();

        Control.ImeOptions = ImeAction.Done;
        Control.EditorAction += OnEditorAction;
    }

    private void OnEditorAction(object sender, TextView.EditorActionEventArgs e)
    {
        e.Handled = false;
        if (e.ActionId == ImeAction.Done)
        {
            Control.ClearFocus();
            e.Handled = true;
            InputMethodManager imm = (InputMethodManager)Control.Context.GetSystemService(Context.InputMethodService);
            imm.HideSoftInputFromWindow(Control.WindowToken, 0);
        }
    }
...

The effect :

enter image description here

About iOS to achieve that , you can refer to follow code :

[assembly: ExportRenderer(typeof(Editor), typeof(CustomEditorRenderer))]
namespace AppEntryTest.iOS
{
    class CustomEditorRenderer : EditorRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<Editor> e)
        {
            base.OnElementChanged(e);

            Control.ReturnKeyType = UIReturnKeyType.Done;
        }


        protected override bool ShouldChangeText(UITextView textView, NSRange range, string text)
        {
            if (text == "\n")
            {
                textView.ResignFirstResponder();
                return false;
            }

            return true;
        }
    }
}

The effect :

enter image description here

====================================Update================================

If need to wrap text in Android , you can set background for EditTextView

Adding bg_gray_border.xml in Resources/drawable folder :

<?xml version="1.0" encoding="utf-8" ?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
  <solid android:color="#ffffff" />
  <stroke
      android:width="1dp"
      android:color="#DEDEDE" />
  <corners android:radius="6dp" />
</shape>

Used in Renderer class :

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

    Control.SetSingleLine();
    Control.SetBackgroundResource(Resource.Drawable.bg_gray_border);

    Control.ImeOptions = ImeAction.Done;
    Control.EditorAction += OnEditorAction;
        
}
...

The effect :

enter image description here

Add wapped text in iOS ,

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

    Control.ReturnKeyType = UIReturnKeyType.Done;

    Control.Layer.BorderColor =UIColor.Gray.CGColor;

    Control.Layer.BorderWidth = 1;

    Control.Layer.CornerRadius = 5;
}
...

The effect :

enter image description here

Here is the sample project .

Junior Jiang
  • 12,430
  • 1
  • 10
  • 30
  • The Android version works, but now the editor doesn't wrap its text, and the iOS version gave me a null reference exception because the Control on OnElementChanged() didn't have a value. – Collin Vesel Jul 07 '20 at 14:40
  • @CollinVesel Okey , you need to check whether modify the `[assembly: ExportRenderer(typeof(Editor), typeof(CustomEditorRenderer))]` to fit your project in iOS . I'm not understanding the editor not wrapping its text in android, you can share your wanted effect with an image here . – Junior Jiang Jul 08 '20 at 01:47
  • I want to do something like this ![Valid XHTML](https://imgur.com/a/oo6QpNp). Basically, I either want an entry that can wrap text like in the comment box in the picture, or an editor that only lets you type one line of text but still wraps it. Right now, your code seems to effectively turn a regular editor into a regular entry. Oh, and as for the assembly attribute, it looks like it should work according to Microsoft’s documentation. I had my custom view in the .NET project and the custom Renderer a for it in the platform-specific projects, which looked like what they did. – Collin Vesel Jul 08 '20 at 12:15
  • @CollinVesel You can share your ios renderer code in question , I will check that . – Junior Jiang Jul 09 '20 at 01:54
  • @CollinVesel Hi , I have updated answer for Android that can wrap text in EditTextView . I'm interesting that if used `Xamarin.Forms.Editor` ,iOS will not show wrapped text , you need to add border for iOS . If not minding using `Xamarin.Forms.Entry` , this will show wrapped text in iOS . – Junior Jiang Jul 09 '20 at 02:08
  • Let me see if I understand correctly. It looked like you said that adding a background would make the editor wrap the text, but that didn’t work (at least on my emulator which is running Android 8.1). Also, I added my iOS custom tenderer to the post. – Collin Vesel Jul 09 '20 at 17:33
  • @CollinVesel Yeah ,there is no problem about ios renderer . What's the version of Xamarin Forms used in your project ? And you can share a new created sample project with custom editor here , I will check that in my local site . Or you can create a new project tested in your PC to check whehther it works . My android device tested is 9.0 . – Junior Jiang Jul 10 '20 at 01:29
  • I’m using Xamarin.Forma 4.7 – Collin Vesel Jul 10 '20 at 01:31
  • @CollinVesel Okey , I will share a new created project here , later you can test in your local site to check where problem is . – Junior Jiang Jul 10 '20 at 01:38
  • @CollinVesel Here is the sample link : https://www.dropbox.com/s/5qj1bduxg015mw8/AppEditor.rar?dl=0 – Junior Jiang Jul 10 '20 at 01:49
  • My code looks exactly like yours for the Android version, so I have no idea what the problem is, but I figured out a solution to do what I wanted to do that doesn’t involve custom renderers. Thank you for your help, though. – Collin Vesel Jul 10 '20 at 18:42
  • @CollinVesel Glad found the solution . You can share your solution in answer later. If my answer be helopful ,remember to vote it up when you have time :-) – Junior Jiang Jul 13 '20 at 02:31
  • Actually, I realized it wouldn’t work and I’m trying to figure out something else. I’m trying to figure out how to format the file that the checklist is going to be saved as, and I wanted to use newlines and tabs as the delimiters, but now I’m thinking about using markup if that would be a good idea. – Collin Vesel Jul 13 '20 at 14:51
  • @CollinVesel If the reply is helpful, please do not forget to accept it as answer( click the ✔ in the upper left corner of this answer), it will help others who have similar issue. :-) – Junior Jiang Jul 31 '20 at 08:46