Change return to be next/done key in Xamarin Forms Shared Project
Asked Answered
O

4

32

Is it possible to change the text in the 'return' key on the keyboard to be either 'next' or 'done'? I have a login form with username and password. I want the return key to say 'next' when on the username field and then 'done' when on the password field but haven't seen anyway of doing this. This is for a shared project, android and iOS.

Username

Password

Olein answered 25/5, 2016 at 18:56 Comment(1)
Just use the property "ReturnType" in your Entry.Selfsufficient
S
31

A custom EntryRenderer can handle changing the keyboard return key description.

  • iOS : UITextField has a ReturnKeyType property that you can set to a preassigned list (see UIReturnType enum).

  • Android : EntryEditText has a ImeOptions property that controls what the "Action" button on the keyboard does and a SetImeActionLabel method that you can use to set any text string for it.

enter image description here

Usage Example of the custom Entry/EntryRenderer:

new EntryExt {
    Text = "Next Key",
    ReturnKeyType = ReturnKeyTypes.Next
},
new EntryExt {
    Text = "Done Key",
    ReturnKeyType = ReturnKeyTypes.Done
}

A Xamarin.Forms custom Entry class:

namespace YourNameSpaceHere
{
    public class EntryExt : Entry
    {
        public const string ReturnKeyPropertyName = "ReturnKeyType";

        public EntryExt() { }

        public static readonly BindableProperty ReturnKeyTypeProperty = BindableProperty.Create(
            propertyName: ReturnKeyPropertyName,
            returnType: typeof(ReturnKeyTypes),
            declaringType: typeof(EntryExt),
            defaultValue: ReturnKeyTypes.Done);

        public ReturnKeyTypes ReturnKeyType
        {
            get { return (ReturnKeyTypes)GetValue(ReturnKeyTypeProperty); }
            set { SetValue(ReturnKeyTypeProperty, value); }
        }
    }

    // Not all of these are support on Android, consult EntryEditText.ImeOptions
    public enum ReturnKeyTypes : int
    {
        Default,
        Go,
        Google,
        Join,
        Next,
        Route,
        Search,
        Send,
        Yahoo,
        Done,
        EmergencyCall,
        Continue
    }
}

iOS custom EntryRenderer:

[assembly: ExportRenderer(typeof(Entry), typeof(EntryExtRenderer_iOS))]
namespace KeyboardDone.iOS
{
    public class EntryExtRenderer_iOS : EntryRenderer
    {
        public EntryExtRenderer_iOS() { }

        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);
            if ((Control != null) && (e.NewElement != null))
                Control.ReturnKeyType = (e.NewElement as EntryExt).ReturnKeyType.GetValueFromDescription();
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);
            if (e.PropertyName == EntryExt.ReturnKeyPropertyName)
            {
                D.WriteLine($"{(sender as EntryExt).ReturnKeyType.ToString()}");
                Control.ReturnKeyType = (sender as EntryExt).ReturnKeyType.GetValueFromDescription();
            }
        }
    }

    public static class EnumExtensions
    {
        public static UIReturnKeyType GetValueFromDescription(this ReturnKeyTypes value)
        {
            var type = typeof(UIReturnKeyType);
            if (!type.IsEnum) throw new InvalidOperationException();
            foreach (var field in type.GetFields())
            {
                var attribute = Attribute.GetCustomAttribute(field,
                    typeof(DescriptionAttribute)) as DescriptionAttribute;
                if (attribute != null)
                {
                    if (attribute.Description == value.ToString())
                        return (UIReturnKeyType)field.GetValue(null);
                }
                else
                {
                    if (field.Name == value.ToString())
                        return (UIReturnKeyType)field.GetValue(null);
                }
            }
            throw new NotSupportedException($"Not supported on iOS: {value}");
        }
    }
}

Android custom EntryRenderer:

[assembly: ExportRenderer(typeof(Entry), typeof(EntryExtRenderer_Droid))]
namespace KeyboardDone.Droid
{
    public class EntryExtRenderer_Droid : EntryRenderer
    {
        public EntryExtRenderer_Droid() { }

        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);
            if ((Control != null) && (e.NewElement != null))
            {
                var entryExt = (e.NewElement as EntryExt);
                Control.ImeOptions = entryExt.ReturnKeyType.GetValueFromDescription();
                // This is hackie ;-) / A Android-only bindable property should be added to the EntryExt class 
                Control.SetImeActionLabel(entryExt.ReturnKeyType.ToString(), Control.ImeOptions);
            }
        }
        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);
            if (e.PropertyName == EntryExt.ReturnKeyPropertyName)
            {
                var entryExt = (sender as EntryExt);
                Control.ImeOptions = entryExt.ReturnKeyType.GetValueFromDescription();
                // This is hackie ;-) / A Android-only bindable property should be added to the EntryExt class 
                Control.SetImeActionLabel(entryExt.ReturnKeyType.ToString(), Control.ImeOptions);
            }
        }

    }
    public static class EnumExtensions
    {
        public static ImeAction GetValueFromDescription(this ReturnKeyTypes value)
        {
            var type = typeof(ImeAction);
            if (!type.IsEnum) throw new InvalidOperationException();
            foreach (var field in type.GetFields())
            {
                var attribute = Attribute.GetCustomAttribute(field,
                    typeof(DescriptionAttribute)) as DescriptionAttribute;
                if (attribute != null)
                {
                    if (attribute.Description == value.ToString())
                        return (ImeAction)field.GetValue(null);
                }
                else
                {
                    if (field.Name == value.ToString())
                        return (ImeAction)field.GetValue(null);
                }
            }
            throw new NotSupportedException($"Not supported on Android: {value}");
        }
    }
}
Secunda answered 25/5, 2016 at 21:39 Comment(11)
Quick question though, where does the .GetValueFromDescription() come from? It's not showing up for meOlein
@Shane Ahhh... sorry about that... that is an enum extension method, same as this SO answer.. https://mcmap.net/q/65303/-get-enum-from-description-attribute-duplicate I am not at a computer to access my source but that answer is the one that I use (and reuse) from years ago. Let my know if you have any problems with it...Secunda
So I threw that method into its own static class (not sure if thats the right thing to do or not) and it got rid of the error but now I'm getting The type arguments for method GetValueFromDescription(this string) cannot be inferred from the usage. Try specifying the type arguments explicitly.. any ideas why?Olein
@Shane , I explictly typed the enums to remove the .ToString(), doing that with a straight copy/paste of that EnumEx code you can : Control.ReturnKeyType = EnumEx.GetValueFromDescription<UIReturnKeyType>((sender as EntryExt).ReturnKeyType.ToString()); That is for iOS, for Android type is an ImeAction ...Secunda
@Shane I'll update the answer later to include the GetValueFromDescription codeSecunda
Let us continue this discussion in chat.Olein
@Shane Tried chat but must have missed you, I added that source to my answer.. but it would be cleaner to put it as a generic enum extension in a shared project.... but this works also..Secunda
What about UWP? ;)Orsola
Sorry, no UWP (-$), I only do iOS/Android (+$) ;-)Secunda
I believe there is a typo in your Android renderer that can lead to "unhandled exceptions": if ((Control != null) & (e.NewElement != null)) should be if ((Control != null) && (e.NewElement != null)).Orella
@derekmx271 Good catch ;-)Secunda
S
21

Yes latest Xamarin Forms allowed directly ReturnType as property, just need to add ReturnType in Xaml

 <Entry x:Name="myEntry" ReturnType="Done"/>
Shillyshally answered 3/1, 2019 at 13:57 Comment(1)
That's true and very useful, unfortunately it doesn't work for the Numeric keyboard on iOS despite it working fine on the Numeric keyboard for Android. I think the custom renderer for iOS would still be needed if you are in the same scenario as me with a Numeric keyboard.Soave
T
15

Here is an alternative approach, but similar to the solution of SushiHangover. It includes UWP support:

ReturnType.cs in PCL

public enum ReturnType
{
    Go,
    Next,
    Done,
    Send,
    Search
}

BaseEntry.cs in PCL

public class BaseEntry : Entry
{
    // Need to overwrite default handler because we cant Invoke otherwise
    public new event EventHandler Completed;

    public static readonly BindableProperty ReturnTypeProperty = BindableProperty.Create(
        nameof(ReturnType),
        typeof(ReturnType),
        typeof(BaseEntry),
        ReturnType.Done, 
        BindingMode.OneWay
    );

    public ReturnType ReturnType
    {
        get { return (ReturnType)GetValue(ReturnTypeProperty); }
        set { SetValue(ReturnTypeProperty, value); }
    }

    public void InvokeCompleted()
    {
        if (this.Completed != null)
            this.Completed.Invoke(this, null);
    }
}

BaseEntryRenderer.cs for Android

public class BaseEntryRenderer : EntryRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
    {
        base.OnElementChanged(e);

        BaseEntry entry = (BaseEntry)this.Element;

        if(this.Control != null)
        {
            if(entry != null)
            {
                SetReturnType(entry);

                // Editor Action is called when the return button is pressed
                Control.EditorAction += (object sender, TextView.EditorActionEventArgs args) =>
                {
                    if (entry.ReturnType != ReturnType.Next)
                        entry.Unfocus();

                    // Call all the methods attached to base_entry event handler Completed
                    entry.InvokeCompleted();
                };
            }
        }
    }

    private void SetReturnType(BaseEntry entry)
    {
        ReturnType type = entry.ReturnType;

        switch (type)
        {
            case ReturnType.Go:
                Control.ImeOptions = ImeAction.Go;
                Control.SetImeActionLabel("Go", ImeAction.Go);
                break;
            case ReturnType.Next:
                Control.ImeOptions = ImeAction.Next;
                Control.SetImeActionLabel("Next", ImeAction.Next);
                break;
            case ReturnType.Send:
                Control.ImeOptions = ImeAction.Send;
                Control.SetImeActionLabel("Send", ImeAction.Send);
                break;
            case ReturnType.Search:
                Control.ImeOptions = ImeAction.Search;
                Control.SetImeActionLabel("Search", ImeAction.Search);
                break;
            default:
                Control.ImeOptions = ImeAction.Done;
                Control.SetImeActionLabel("Done", ImeAction.Done);
                break;
        }
    }
}

BaseEntryRenderer.cs for iOS

public class BaseEntryRenderer : EntryRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
    {
        base.OnElementChanged(e);

        BaseEntry entry = (BaseEntry)this.Element;

        if (this.Control != null)
        {
            if(entry != null)
            {
                SetReturnType(entry);

                Control.ShouldReturn += (UITextField tf) =>
                {
                    entry.InvokeCompleted();
                    return true;
                };
            }
        }
    }

    private void SetReturnType(BaseEntry entry)
    {
        ReturnType type = entry.ReturnType;

        switch (type)
        {
            case ReturnType.Go:
                Control.ReturnKeyType = UIReturnKeyType.Go;
                break;
            case ReturnType.Next:
                Control.ReturnKeyType = UIReturnKeyType.Next;
                break;
            case ReturnType.Send:
                Control.ReturnKeyType = UIReturnKeyType.Send;
                break;
            case ReturnType.Search:
                Control.ReturnKeyType = UIReturnKeyType.Search;
                break;
            case ReturnType.Done:
                Control.ReturnKeyType = UIReturnKeyType.Done;
                break;
            default:
                Control.ReturnKeyType = UIReturnKeyType.Default;
                break;
        }
    }
}

BaseEntryRenderer.cs for UWP

public class BaseEntryRenderer : EntryRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
    {
        base.OnElementChanged(e);

        BaseEntry entry = (BaseEntry)this.Element;

        if(this.Control != null)
        {
            if(entry != null)
            {
                this.Control.KeyDown += (object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs eventArgs) =>
                {
                    if (eventArgs.Key == Windows.System.VirtualKey.Enter)
                    {
                        entry.InvokeCompleted();
                        // Make sure to set the Handled to true, otherwise the RoutedEvent might fire twice
                        eventArgs.Handled = true;
                    }
                };
            }
        }
    }
}

On UWP it seems not be possible to change the return key to next/done/... You need to add ExportRenderer attributes yourself for all custom renderers.

Usage

XAML file

<renderer:BaseEntry x:Name="username" Text="Username" ReturnType="Next" />
<renderer:BaseEntry x:Name="password" Text ="Password" IsPassword="true" ReturnType="Done" />

Code behind file:

this.username.Completed += (object sender, EventArgs e) => this.password.Focus();

Based on this source.

Tartarus answered 28/11, 2016 at 14:7 Comment(1)
You forgot the line to associate the platform-specific renderers with the BaseEntry class. Here it is: [assembly: ExportRenderer(typeof(BaseEntry), typeof(BaseEntryRenderer))]Miterwort
S
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.

Splat answered 11/7, 2018 at 20:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.