Search code examples
xamarinxamarin.formsxamarin.android

Focus problem on pages with no entry in Xamarin Forms


I am struggling with a focus problem with Xamarin Forms Android.
We usually run this application on handhelds with external keyboard. My application has a navigation bar on the top of the app and the entry usually is on the bottom.
In the navigation bar there is the "Escape" image button to return back on the previous page and some other buttons to do special functions.
Every page has entries on the footer but rarely some pages has no entry. While the pages with entry is focused correctly and when i press the enter key the EnterCommand associated to the "ReturnCommand" of the entry is fired correctly, when there is no entry the first element is focused and the enter key fires the "Click/Press" of the "Escape" image button giving the result of return back to the previous page.

How can i prevent the image button to be focused or how can i focus other controls on the page?

This is the base style of every page:

<Grid HorizontalOptions="FillAndExpand"
      VerticalOptions="FillAndExpand"
      BackgroundColor="{StaticResource NavigationBarColor}"
      Padding="0,5">
    <Grid.Resources>
        <Style TargetType="ImageButton">
            <Style.Setters>
                <Setter Property="Padding"
                        Value="0" />
                <Setter Property="BackgroundColor"
                        Value="Transparent" />
                <Setter Property="VerticalOptions"
                        Value="CenterAndExpand" />
            </Style.Setters>
        </Style>
    </Grid.Resources>

    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="33" />
        <ColumnDefinition />
        <ColumnDefinition Width="33" />
        <ColumnDefinition Width="33" />
    </Grid.ColumnDefinitions>

    <ImageButton Command="{TemplateBinding EscapeCommand}"
                 Source="{helpers:ImageResource Previous}"
                 IsVisible="{Binding IsEnabled, Source={RelativeSource Mode=Self}}" />

    <ImageButton Command="{TemplateBinding OpenMenuCommand}"
                 Source="{helpers:ImageResource Menu}"
                 IsVisible="{Binding IsEnabled, Source={RelativeSource Mode=Self}}" />

    <Label Text="{TemplateBinding ViewModel.Title}"
           Grid.Column="1"
           FontSize="Medium"
           Padding="0,2"
           FontAttributes="Bold"
           MaxLines="2"
           LineBreakMode="TailTruncation"
           VerticalOptions="FillAndExpand"
           VerticalTextAlignment="Center" />

    <ImageButton Command="{TemplateBinding ApriOpzioniCommand}"
                 Source="{helpers:ImageResource More}"
                 IsVisible="{Binding IsEnabled, Source={RelativeSource Mode=Self}}"
                 Grid.Column="2" />

    <ImageButton Command="{TemplateBinding ViewModel.OkCommand}"
                 Source="{helpers:ImageResource Next}"
                 IsVisible="{Binding IsEnabled, Source={RelativeSource Mode=Self}}"
                 IsEnabled="False"
                 Grid.Column="3" />
</Grid>
<!-- End NavigationBar -->

<ScrollView x:Name="scrollViewContent"
            Style="{StaticResource ScrollViewBaseStyle}"
            Grid.Row="1">
    <StackLayout VerticalOptions="FillAndExpand"
                 HorizontalOptions="FillAndExpand"
                 Spacing="0">

        <!-- Begin Header -->
        <Frame Style="{StaticResource FrameBaseStyle}"
               CornerRadius="6">
            <ContentView Content="{TemplateBinding Header}"
                         Style="{StaticResource ContentViewStyle}"
                         TabIndex="2"
                         IsTabStop="False" />
        </Frame>
        <!-- End Header -->

        <!-- Begin Content -->
        <ContentPresenter Style="{StaticResource ContentPresenterBaseStyle}"
                          TabIndex="1" />
        <!-- End Content -->

    </StackLayout>
</ScrollView>

<!-- Begin Footer -->
<ContentView Content="{TemplateBinding Footer}"
             Grid.Row="2"
             Margin="5"
             TabIndex="0"
             Style="{StaticResource ContentViewStyle}" />

The view model base of every page:

public abstract class ContentPageBase<TViewModel> : ReactiveContentPage<TViewModel>, IAlertPage, ITemplatedPage, IDisposable
    where TViewModel : NavigationViewModelBase
{
    private bool _initialized;
    private readonly Subject<int> _pagesCount;
    private readonly Subject<bool> _isOpzioniDisponibili;

    protected ContentPageBase(IKeyboardService keyboardListener)
    {
        KeyboardListener = keyboardListener ?? throw new ArgumentNullException(nameof(keyboardListener));

        _pagesCount = new Subject<int>();
        _isOpzioniDisponibili = new Subject<bool>();

        ApriOpzioniCommand = ReactiveCommand.Create(
            MostraNascondiOpzioni,
            _isOpzioniDisponibili);
        ChiudiOpzioniCommand = ReactiveCommand.Create(
            NascondiOpzioni,
            _isOpzioniDisponibili);

        EscapeCommand = ReactiveCommand.CreateFromTask(
            OnEscapePressedAsync,
            this.WhenAnyValue(v => v.IsOpzioniVisible)
                .CombineLatest(_pagesCount, (v1, v2) => v1 || v2 > 1));

        OpenMenuCommand = ReactiveCommand.Create(
            OnMenuPressed,
            _pagesCount.Select(c => c == 1 && !IsMenuShown));

        InitLayout();

        this.WhenActivated(disposables =>
        {
            RegisterInteractions(disposables);

            InitBindings(disposables);

            WhenActivated(disposables);
            WhenViewFocused();

            _initialized = true;
        });
    }

    public ReactiveCommand<Unit, Unit> OpenMenuCommand { get; }

    public ReactiveCommand<Unit, Unit> ApriOpzioniCommand { get; }

    public ReactiveCommand<Unit, Unit> ChiudiOpzioniCommand { get; }

    public ReactiveCommand<Unit, Unit> EscapeCommand { get; }

    public bool IsMenuShown =>
        (Application.Current.MainPage as MasterDetailPage)?.IsPresented == true;

    protected IKeyboardService KeyboardListener { get; }

    #region Bindable properties

    public static readonly BindableProperty HeaderBackgroundColorProperty = BindableProperty.Create(
        propertyName: nameof(HeaderBackgroundColor),
        returnType: typeof(Color),
        declaringType: typeof(Color),
        defaultValue: Color.FromHex("#363636"),
        defaultBindingMode: BindingMode.OneTime);

    public static readonly BindableProperty HeaderPaddingProperty = BindableProperty.Create(
        propertyName: nameof(HeaderPadding),
        returnType: typeof(Thickness),
        declaringType: typeof(ContentPageBase<>),
        defaultValue: new Thickness(10),
        defaultBindingMode: BindingMode.OneTime);

    public static readonly BindableProperty HeaderMarginProperty = BindableProperty.Create(
        propertyName: nameof(HeaderMargin),
        returnType: typeof(Thickness),
        declaringType: typeof(ContentPageBase<>),
        defaultValue: new Thickness(10),
        defaultBindingMode: BindingMode.OneTime);

    public static readonly BindableProperty HeaderProperty = BindableProperty.Create(
        propertyName: nameof(Header),
        returnType: typeof(View),
        declaringType: typeof(ContentPageBase<>),
        defaultValue: null,
        defaultBindingMode: BindingMode.OneTime);

    public static readonly BindableProperty FooterProperty = BindableProperty.Create(
        propertyName: nameof(Footer),
        returnType: typeof(View),
        declaringType: typeof(ContentPageBase<>),
        defaultValue: null,
        defaultBindingMode: BindingMode.OneTime);

    public static readonly BindableProperty OpzioniProperty = BindableProperty.Create(
        propertyName: nameof(Opzioni),
        returnType: typeof(View),
        declaringType: typeof(ContentPageBase<>),
        defaultValue: null,
        defaultBindingMode: BindingMode.OneTime);

    public static readonly BindableProperty IsOpzioniVisibleProperty = BindableProperty.Create(
        propertyName: nameof(IsOpzioniVisible),
        returnType: typeof(bool),
        declaringType: typeof(ContentPageBase<>),
        defaultValue: false,
        defaultBindingMode: BindingMode.OneWay);

    public static readonly BindableProperty ViewCellTemplateProperty = BindableProperty.Create(
        propertyName: nameof(ViewCellTemplate),
        returnType: typeof(DataTemplate),
        declaringType: typeof(ContentPageBase<>),
        defaultValue: null,
        defaultBindingMode: BindingMode.OneTime);

    public static readonly BindableProperty RiepilogoViewProperty = BindableProperty.Create(
        propertyName: nameof(RiepilogoView),
        returnType: typeof(IViewFor),
        declaringType: typeof(ContentPageBase<>),
        defaultValue: null,
        defaultBindingMode: BindingMode.OneTime);

    public Color HeaderBackgroundColor
    {
        get => (Color)GetValue(HeaderBackgroundColorProperty);
        set => SetValue(HeaderBackgroundColorProperty, value);
    }

    public Thickness HeaderMargin
    {
        get => (Thickness)GetValue(HeaderMarginProperty);
        set => SetValue(HeaderMarginProperty, value);
    }

    public Thickness HeaderPadding
    {
        get => (Thickness)GetValue(HeaderPaddingProperty);
        set => SetValue(HeaderPaddingProperty, value);
    }

    public View Header
    {
        get => (View)GetValue(HeaderProperty);
        set => SetValue(HeaderProperty, value);
    }

    public View Footer
    {
        get => (View)GetValue(FooterProperty);
        set => SetValue(FooterProperty, value);
    }

    public View Opzioni
    {
        get => (View)GetValue(OpzioniProperty);
        set => SetValue(OpzioniProperty, value);
    }

    public bool IsOpzioniVisible
    {
        get => (bool)GetValue(IsOpzioniVisibleProperty);
        set => SetValue(IsOpzioniVisibleProperty, value);
    }

    public DataTemplate ViewCellTemplate
    {
        get => (DataTemplate)GetValue(ViewCellTemplateProperty);
        set => SetValue(ViewCellTemplateProperty, value);
    }

    public IViewFor RiepilogoView
    {
        get => (IViewFor)GetValue(RiepilogoViewProperty);
        set => SetValue(RiepilogoViewProperty, value);
    }

    #endregion Bindable properties

    private void InitBindings(CompositeDisposable disposables)
    {
        KeyboardListener.KeysPressed
            .Where(k => k.Key == KeyCode.F10)
            .Select(_ => Unit.Default)
            .InvokeCommand(ApriOpzioniCommand)
            .DisposeWith(disposables);

        KeyboardListener.KeysPressed
            .Where(k => k.Key == KeyCode.Escape)
            .Select(_ => Unit.Default)
            .InvokeCommand(EscapeCommand)
            .DisposeWith(disposables);

        this.OneWayBind(ViewModel, vm => vm.Title, v => v.Title)
            .DisposeWith(disposables);

        //esegue il comando alla prima attivazione o sempre a seconda della proprietà nel ViewModel
        if (ViewModel.OnActivateCommand != null && (!_initialized || !ViewModel.ActivateOnce))
        {
            Observable.Return(Unit.Default)
                .InvokeCommand(this, v => v.ViewModel.OnActivateCommand)
                .DisposeWith(disposables);
        }
    }

    private void InitLayout()
    {
        NavigationPage.SetHasNavigationBar(this, false);
        ControlTemplate = (ControlTemplate)Application.Current.Resources["MainPageTemplate"];
    }

    protected abstract void WhenActivated(CompositeDisposable disposables);

    protected abstract void WhenViewFocused();

    protected override async void OnAppearing()
    {
        try
        {
            _pagesCount.OnNext(Application.Current.MainPage is MasterDetailPage ? Navigation.NavigationStack.Count : 0);
            _pagesCount.OnCompleted();

            _isOpzioniDisponibili.OnNext(Opzioni != null);
            _isOpzioniDisponibili.OnCompleted();

            if (IsOpzioniVisible)
            {
                NascondiOpzioni();
            }
            if (Opzioni is IBindable view)
            {
                view.InitBindings();
            }

            if (RiepilogoView is IBindable viewRiepilogo)
            {
                viewRiepilogo.InitBindings();
            }

            var scroller = (ScrollView)GetTemplateChild("scrollViewContent");

            await scroller.ScrollToAsync(0, scroller.Height, false).ConfigureAwait(true);
        }
        catch (Exception ex)
        {
            await DisplayExceptionAsync(ex).ConfigureAwait(true);
        }
    }

    protected override void OnDisappearing()
    {
        if (Opzioni is IBindable view)
        {
            view.DisposeBindings();
        }

        if (RiepilogoView is IBindable viewRiepilogo)
        {
            viewRiepilogo.DisposeBindings();
        }

        base.OnDisappearing();
    }

    private async Task OnEscapePressedAsync()
    {
        // Se il menù è aperto
        if (IsOpzioniVisible)
        {
            NascondiOpzioni();
        }
        // Se non sono la pagina root dello stack di navigazione
        else if (Navigation != null && Navigation.NavigationStack.Count > 1)
        {
            await ViewModel.NavigationService.NavigateBackAsync().ConfigureAwait(true);
        }
    }

    private void OnMenuPressed() =>
        ((MasterDetailPage)Application.Current.MainPage).IsPresented = true;

    private void MostraNascondiOpzioni()
    {
        if (Opzioni != null)
        {
            if (IsOpzioniVisible)
            {
                NascondiOpzioni();
            }
            else
            {
                MostraOpzioni();
            }
        }
    }

    public void MostraOpzioni() => IsOpzioniVisible = true;

    public void NascondiOpzioni()
    {
        IsOpzioniVisible = false;

        WhenViewFocused();
    }
}

These is a page without entry and it is affected with the problem:

<?xml version="1.0" encoding="utf-8" ?>
<views:ContentPageBase xmlns="http://xamarin.com/schemas/2014/forms"
                       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                       xmlns:vm="clr-namespace:MadLab.Spectrum.ViewModels.Inventario;assembly=MadLab.Spectrum.ViewModels"
                       xmlns:views="clr-namespace:MadLab.Spectrum.Views"
                       xmlns:vCommon="clr-namespace:MadLab.Spectrum.Views.Common"
                       xmlns:vDatiLotto="clr-namespace:MadLab.Spectrum.Views.Common.DatiAggiuntiviLotto"
                       x:Class="MadLab.Spectrum.Views.Inventario.RiepilogoConfermaPage"
                       x:TypeArguments="vm:RiepilogoConfermaViewModel">
    <ContentPage.Content>
        <Label x:Name="lblAvvisoQuantita"
                        VerticalOptions="Center"
                        HorizontalOptions="Center"
                        FontAttributes="Bold"
                        FontSize="Medium"
                        HorizontalTextAlignment="Center" />
    </ContentPage.Content>
    <views:ContentPageBase.Footer>
        <StackLayout Orientation="Horizontal"
                     VerticalOptions="FillAndExpand"
                     HorizontalOptions="CenterAndExpand"
                     Margin="10">
            <Image Source="{helper:ImageResource EnterKeyGreen32}">
                <Image.GestureRecognizers>
                    <TapGestureRecognizer x:Name="gestRecognizer" />
                </Image.GestureRecognizers>
            </Image>

            <Label Text="{Binding Text, Source={x:Reference view}}"
                   FontSize="Large"
                   FontAttributes="Bold"
                   TextColor="{StaticResource GreenColor}"
                   HorizontalOptions="CenterAndExpand"
                   VerticalOptions="CenterAndExpand"
                   VerticalTextAlignment="Center"
                   Padding="0,0,0,5" />
        </StackLayout>
    </views:ContentPageBase.Footer>
</views:ContentPageBase>

I have tried IsTabStop = false, TabIndex and all hints that i found here but nothing works, it keeps focus it and pressing enter gives same result. When i press enter nothing should be done (seems the focus is mandatory on Android).


Solution

  • The github issue related to this problem is here on github.

    Doing the proposed suggestion fixed the problem.