Specify a default button in Xamarin.Forms
Asked Answered
L

4

7

In Xamarin.Forms, how do you designate a button as the default button for a page?

For example, on UWP the click handler for a DefaultButton should fire when the user presses the Enter key while the page has focus.

Loss answered 1/5, 2017 at 17:57 Comment(2)
Since a platform-agnostic mechanism for specifying the default button doesn't seem to exist out-of-the-box in Xamarin.Forms, I submitted a suggestion to that effect.Loss
If you're using an entry control, the Completed event handler will fire when pressing enter. Note that I've been using this for iOS and Android. Not sure about other uses.Indicia
I
5

Assuming you are trying to replicate the behavior as noted in your first comment to Alessandro, you want to declare the Completed field of your Entry control to use the same event handler as your button.

For example, in XAML:

<Entry Placeholder="Enter PIN Here"
       Completed="DefaultButton_Clicked"/>

<Button Text="OK"
        Clicked="DefaultButton_Clicked"/>

Then in your code behind:

void DefaultButton_Clicked(object sender, EventArgs e)
{
    //Do stuff
}

If you are looking to do this all in code behind like you have answered, I suggest doing it this way so you are able to unsubscribe your events. You'll find working with anonymous functions to be more of a pain. You should subscribe and unsubscribe your events in OnAppearing/OnDisappearing.

void DefaultButton_Clicked(object sender, EventArgs e)
{
    if (model.MyCommand.CanExecute(null))
            model.MyCommand.Execute(null);
}

protected override void OnAppearing()
{
    base.OnAppearing();
    foreach (var child in ((StackLayout)Content).Children)
    {
        if (child is Entry entry)
            entry.Completed += DefaultButton_Clicked;
    }
}

protected override void OnDisappearing()
{
    base.OnDisappearing();
    foreach (var child in ((StackLayout)Content).Children)
    {
        if (child is Entry entry)
            entry.Completed -= DefaultButton_Clicked;
    }
}
Indicia answered 2/5, 2017 at 22:49 Comment(2)
Is there a similar workaround for when the default button uses a Command with a CanExecute filter?Loss
Not directly I don't think. Check this out: #4898275Indicia
L
0

I ended up writing code that wires up each Entry to conditionally invoke the default button's command. In my case, each Entry is in a single StackLayout at the page root, so the page's constructor has code like this:

foreach (var child in ((StackLayout)Content).Children)
    if (child is Entry entry)
        entry.Completed += delegate {
            if (model.MyCommand.CanExecute(null))
                model.MyCommand.Execute(null);
        };
Loss answered 3/5, 2017 at 17:50 Comment(3)
Be careful with assigning delegates like this. This will cause a memory leak if you don't -= them.Indicia
@TimothyJames Won't the delegates get garbage collected at the same time as the page?Loss
I'm going to say maybe. I'm still not 100% versed on anonymous functions and garbage collection. I'd suggest reading around to be sure.Indicia
B
0

We ended up implementing an entry behavior for the completed event. We wanted to bind it from .xaml and to be able to both navigate between fields and submit on the last field. It took some research, so I'm sharing here in the hopes it will be useful for someone else, too:

            <Entry TabIndex="0" Placeholder="{ui:Resource ID_STR_USERNAME}" Text="{Binding Username, Mode=TwoWay}" Style="{StaticResource EntryText}">
                <Entry.Behaviors>
                    <behaviors:EntryCompletedBehavior />
                </Entry.Behaviors>
            </Entry>
            <Entry TabIndex="1" Placeholder="{ui:Resource ID_STR_PASSWORD_HINT}" IsPassword="True" Text="{Binding Password.Value, Mode=TwoWay}" Style="{StaticResource EntryText}">
                <Entry.Behaviors>
                    <behaviors:EntryTextChangeCommandBehavior Command="{Binding Password.ValidateCommand}" />
                    <behaviors:EntryCompletedBehavior Command="{Binding LoginCommand, Mode=TwoWay}" CommandParameter="{Binding Password}" />
                </Entry.Behaviors>
            </Entry>

and the behavior:

public class EntryCompletedBehavior : Behavior<Entry>
{
    public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(EntryCompletedBehavior), null);
    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(EntryCompletedBehavior), null);
    public object CommandParameter
    {
        get { return GetValue(CommandParameterProperty); }
        set { SetValue(CommandParameterProperty, value); }
    }

    protected override void OnAttachedTo(Entry bindable)
    {
        base.OnAttachedTo(bindable);

        if(bindable.BindingContext != null)
            BindingContext = bindable.BindingContext;

        bindable.BindingContextChanged += Bindable_BindingContextChanged;

        bindable.Completed += Bindable_Completed;
    }
    protected override void OnDetachingFrom(Entry bindable)
    {
        base.OnDetachingFrom(bindable);

        bindable.BindingContextChanged -= Bindable_BindingContextChanged;

        bindable.Completed -= Bindable_Completed;
    }

    private void Bindable_Completed(object sender, System.EventArgs e)
    {
        if(sender is Entry entry)
        {
            var ix = entry.TabIndex;
            int allIndexesOnPage;
            var coll = entry.GetTabIndexesOnParentPage(out allIndexesOnPage);
            foreach(var item in coll)
            {
                if(ix < item.Key)
                {
                    item.Value.First().Focus();
                    return;
                }
            }
        }

        if(Command == null)
            return;

        if(Command.CanExecute(CommandParameter))
        {
            Command.Execute(CommandParameter);
        }
    }

    void Bindable_BindingContextChanged(object sender, EventArgs e)
    {
        base.OnBindingContextChanged();

        if(!(sender is BindableObject bindable))
            return;

        BindingContext = bindable.BindingContext;
    }
}

As an added bonus, the behavior can be used on its own, just to navigate between tab-indexed fields.

Based on this post: https://forums.xamarin.com/discussion/124714/how-to-fire-a-complete-event-from-viewmodel

Butadiene answered 24/4, 2019 at 11:22 Comment(0)
S
-1

You have not to say "In Xamarin.Forms, how do you designate a button as the default button for a page?" because Xamarin.Forms visualize only Platform Specific controls.

You should ask "In Android (or iOS) how do you designate a button as the default button for a page?".

I think it is the focused button, so you can try something like

Button b = new Button {Text = "This is the focused button"};
b.Focus();

But I am not sure of this.

Take a look here in Xamarin forum

Subroutine answered 1/5, 2017 at 18:18 Comment(3)
This question does not relate to automatically changing focus. For example, consider the UWP change PIN dialog in Windows 10. The focus rules are all standard (a clicked control gets focus, Tab advances focus, etc.). From any of the three text boxes, pressing Enter causes OK's click handler to be fired, since it is the default button, but focus remains unchanged. Ideally, Xamarin.Forms would provide a platform agnostic way to designate a default button. Since it doesn't appear to, I'd like to use custom renders or something else that would provide the same effect.Loss
@EdwardBrey, you ask a question, answer to your question, down vote an answer and comment the answer. ExcellentSubroutine
If I had an answer to my question, I'd post it as an answer. Unfortunately, I don't. Setting focus on the button isn't the answer, because the point of a default button is that it works when it doesn't have focus. A custom renderer may be part of a solution, but I couldn't get it to work: PageRenderer.Control was always null.Loss

© 2022 - 2024 — McMap. All rights reserved.