NET MAUI - Keep keyboard focused on ListView update

360 Views Asked by At

I have a ListView with an ObservableCollection of Database Entries. When I update the Observable collection the focus of the Entry get lost and the keyboard disappears. Even when i set the focus afterwards manually the keyboard does not appear.

Thats my code.

Page.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:models="clr-namespace:LoginManager.Models"
             x:Class="LoginManager.OverviewPage"
             BackgroundColor="{AppThemeBinding Light={StaticResource LightBackground}, Dark={StaticResource DarkBackground}}">

    <Grid Margin="10">

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0" ColumnSpacing="{OnPlatform WinUI=50, Default=0}" Margin="{OnPlatform WinUI='5,10,10,10', Default='0,-15,-10,-10'}">

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>

            <Border StrokeThickness="5"
                    StrokeShape="RoundRectangle 20"
                    Padding="0,2.5,0,5"
                    Style="{StaticResource Border}"
                    Grid.Column="0"
                    VerticalOptions="Center">

                <Entry x:Name="SearchEntry"
                       Style="{StaticResource Entry}"
                       BackgroundColor="Transparent"
                       FontSize="{OnPlatform WinUI='50', Default='35'}"
                       Placeholder="Suche"
                       HorizontalTextAlignment="Center"
                       VerticalOptions="Center"
                       Margin="15,0,15,0"
                       IsEnabled="{Binding IsNotBusy}"
                       TextChanged="SearchEntry_TextChanged"/>
            </Border>

            <ImageButton x:Name="SortByBtn"
                         Grid.Column="1"
                         Style="{StaticResource ImageButton}"
                         Source="sort_icon.png"
                         Aspect="AspectFit"
                         BackgroundColor="{AppThemeBinding Light={StaticResource LightAccent}, Dark={StaticResource DarkAccent}}"
                         Margin="0,5,0,5"
                         Padding="15"
                         HeightRequest="115"
                         WidthRequest="115"
                         HorizontalOptions="End"
                         VerticalOptions="Center"
                         IsEnabled="{Binding IsNotBusy}"
                         Pressed="SortByBtn_Pressed"
                         Released="SortByBtn_Released"
                         Clicked="SortByBtn_Clicked"/>
            
        </Grid>

        <Border Grid.Row="1"
                Style="{StaticResource Border}"
                BackgroundColor="{AppThemeBinding Light={StaticResource LightAccent}, Dark={StaticResource DarkAccent}}"
                StrokeThickness="2.5"
                StrokeShape="RoundRectangle 20"
                Padding="2.5"
                Margin="{OnPlatform WinUI='0,7.5,0,7.5', Default='0,0,0,7.5'}"/>

        <ActivityIndicator Grid.Row="2"
                           IsRunning="{Binding IsBusy}"
                           IsVisible="{Binding IsBusy}"
                           ZIndex="1"
                           Margin="100"
                           Color="{AppThemeBinding Light={StaticResource LightPrimary}, Dark={StaticResource DarkPrimary}}"/>

        <ListView x:Name="DatabaseEntriesListView"
                  Grid.Row="2"
                  Margin="20,0"
                  BackgroundColor="Transparent"
                  IsPullToRefreshEnabled="False"
                  HasUnevenRows="True"
                  SeparatorVisibility="None"
                  ItemsSource="{Binding DbEntries}"
                  HorizontalScrollBarVisibility="Never"
                  VerticalScrollBarVisibility="Never"
                  SelectionMode="None"
                  ItemTapped="DatabaseEntriesListView_ItemTapped">

            <ListView.ItemTemplate>
                <DataTemplate x:DataType="models:DbEntry">
                    <ViewCell>
                        <ViewCell.ContextActions>
                            <MenuItem Text="Bearbeiten"/>
                            <MenuItem Text="Löschen"/>
                        </ViewCell.ContextActions>

                        <Border Style="{StaticResource Border}"
                                Stroke="{AppThemeBinding Light={StaticResource LightPrimary}, Dark={StaticResource DarkPrimary}}"
                                BackgroundColor="{AppThemeBinding Light={StaticResource LightSecondary}, Dark={StaticResource DarkSecondary}}"
                                StrokeThickness="5"
                                StrokeShape="RoundRectangle 20"
                                Padding="20"
                                Margin="{OnPlatform WinUI='0,10,0,10', Default='0,2.5,0,2.5'}">
                            <Label Style="{StaticResource Label}"
                                   FontSize="{OnPlatform WinUI='35', Default='25'}"
                                   Text="{Binding Name}"
                                   HorizontalTextAlignment="Center"
                                   VerticalTextAlignment="Center"
                                   MaxLines="1"
                                   LineBreakMode="NoWrap"/>
                        </Border>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</ContentPage>

Page.xaml.cs:

namespace LoginManager;

using LoginManager.Common;
using LoginManager.ViewModels;

public sealed partial class OverviewPage : ContentPage
{
    private readonly OverviewViewModel _vm;

    public OverviewPage(OverviewViewModel vm)
    {
        InitializeComponent();
        BindingContext = _vm = vm;
    }

    protected override async void OnAppearing()
    {
        base.OnAppearing();

        SortByBtn.Scale = 1;

        var loadedSuccessfully = await _vm.GetWholeDb(_vm.CurrentUser);

        if (!loadedSuccessfully)
        {
            await DisplayAlert("Fehler", "Die Einträge konnten nicht geladen werden", "Okay");
            
            await Shell.Current.GoToAsync("..", true);
        }
    }

    private async void SearchEntry_TextChanged(object sender, TextChangedEventArgs e)
    {
        if (!SearchEntry.IsFocused) SearchEntry.Focus();

        await _vm.SearchEntriesBy(e.NewTextValue);

        if (!SearchEntry.IsFocused)
        {
            SearchEntry.Focus();
        }
    }

    private async void SortByBtn_Pressed(object sender, EventArgs e)
    {
        await SortByBtn.ScaleTo(0.95, 100, Easing.Linear);
        await SortByBtn.ScaleTo(1, 100, Easing.Linear);
    }

    private async void SortByBtn_Released(object sender, EventArgs e) => await SortByBtn.ScaleTo(1, 100, Easing.Linear);

    private async void SortByBtn_Clicked(object sender, EventArgs e)
    {
        var chosen = await DisplayActionSheet("Sortieren nach...", null, "Abbrechen",
            "neueste zuerst", "älteste zuerst", "Alphabet aufsteigend", "Alphabet absteigend");

        var sortBy = chosen switch
        {
            "neueste zuerst" => SortBy.IdDESC,
            "älteste zuerst" => SortBy.IdASC,
            "Alphabet aufsteigend" => SortBy.NameASC,
            "Alphabet absteigend" => SortBy.NameDESC,
            _ => SortBy.None
        };

        await _vm.SortEntriesBy(sortBy);
    }

    private void DatabaseEntriesListView_ItemTapped(object sender, ItemTappedEventArgs e)
    {

    }
}

ViewModel.cs:

namespace LoginManager.ViewModels;

using Common;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LoginManager.Models;
using LoginManager.Services;
using System.Collections.ObjectModel;

[QueryProperty(nameof(CurrentUser), nameof(CurrentUser))]
public sealed partial class OverviewViewModel : ObservableObject
{
    private readonly IDatabase _db;

    public ObservableCollection<DbEntry> DbEntries { get; set; } = new();

    public OverviewViewModel(IDatabase db)
    {
        _db = db;
    }

    [ObservableProperty]
    User currentUser;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsNotBusy))]
    bool isBusy;

    public bool IsNotBusy => !IsBusy;

    [RelayCommand]
    public async Task<bool> GetWholeDb(User currentUser)
    {
        await LoadDb(currentUser);

        return DbEntries.Count is not 0;
    }

    [RelayCommand]
    public Task SortEntriesBy(SortBy sortBy)
    {
        IsBusy = true;

        switch (sortBy)
        {
            case SortBy.NameASC:
                DbEntries.SortBy(x => x.Name);
                break;

            case SortBy.NameDESC:
                DbEntries.SortBy(x => x.Name, false);
                break;

            case SortBy.IdASC:
                DbEntries.SortBy(x => x.Id);
                break;

            case SortBy.IdDESC:
                DbEntries.SortBy(x => x.Id, false);
                break;
        }

        IsBusy = false;

        return Task.CompletedTask;
    }

    [RelayCommand]
    public Task SearchEntriesBy(string? query)
    {
        IsBusy = true;

        if (!string.IsNullOrWhiteSpace(query)) DbEntries.SortBy(x => x.Name.Count(x => query.Contains(x)));

        IsBusy = false;

        return Task.CompletedTask;
    }

    private async Task LoadDb(User currentUser)
    {
        IsBusy = true;

        await foreach (var entry in _db.GetWholeAsync(currentUser))
        {
            DbEntries.Add(entry);
        }

        IsBusy = false;
    }
}

I tried setting a Delay for the TextChanged Event but it doesn't work either. Also I don't want the Entry being unfocused and the keyboard going away at all.

1

There are 1 best solutions below

0
Liyun Zhang - MSFT On

When the value of the Entry's IsEnabled property is false. The entry will be unfocusable and the keyboard will disappear.

But in your code, you set public bool IsNotBusy => !IsBusy. When the value of isBusy is true. The entry will be disabled. And when you set the isBusy = true again, the entry is enabled but it will not auto focus.

And for the android, if you call the SearchEntry.Focus(); to make the entry focused, the keyboard will not appear unless the user click the entry. So you can use the following code to force the keyboard appear.

           SearchEntry.Focus();
#if ANDROID
            Android.Views.InputMethods.InputMethodManager inputManager = (Android.Views.InputMethods.InputMethodManager)Android.App.Application.Context.ApplicationContext.GetSystemService(Android.Content.Context.InputMethodService);
            inputManager.ToggleSoftInput(Android.Views.InputMethods.InputMethodManager.ShowForced, Android.Views.InputMethods.HideSoftInputFlags.ImplicitOnly);
#endif