maui
maui copied to clipboard
Unintended focus control occurs in NavigationPage and Shell.
Discussed in https://github.com/dotnet/maui/discussions/14210
Originally posted by cat0363 March 27, 2023 If NavigationPage is specified for MainPage, unintended focus control will occur. If Shell is specified for MainPage, unintended focus control will not occur. To verify this problem, I created the screen layout shown below.
https://github.com/cat0363/Maui-Issue14211
[MainPage.xaml]
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="NavigationPageFocusTest.MainPage">
<Grid RowDefinitions="44,44,150,*" RowSpacing="5">
<StackLayout Grid.Row="0" Orientation="Horizontal">
<Label Text="Role:" VerticalOptions="Center" />
<Picker VerticalOptions="Center">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>President</x:String>
<x:String>Executive Vice President</x:String>
<x:String>Executive Managing Director</x:String>
<x:String>General Manager</x:String>
<x:String>Manager</x:String>
<x:String>Section Chief</x:String>
<x:String>Deputy Section Manager</x:String>
<x:String>Employee</x:String>
</x:Array>
</Picker.ItemsSource>
</Picker>
</StackLayout>
<Button Grid.Row="1" x:Name="btnAdd" Text="Add Employee" BackgroundColor="Blue" TextColor="White" Clicked="btnAdd_Clicked"/>
<ScrollView Grid.Row="2" x:Name="svEmployeeList" Orientation="Vertical">
<StackLayout x:Name="slEmployeeList" Orientation="Vertical" BindableLayout.ItemsSource="{Binding Employees}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="80,*" HeightRequest="44">
<Label Grid.Column="0" Text="{Binding EmployeeId}" VerticalOptions="Center" />
<Entry Grid.Column="1" Text="{Binding EmployeeName}" VerticalOptions="Center" />
</Grid>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</ScrollView>
</Grid>
</ContentPage>
It is a simple screen layout with Picker, Button, Label and Entry in list format. Pressing the Add Employee button adds a new employee to the employee list and scrolls to the bottom of the employee list.
MainPage's internal logic and various classes are shown below.
[MainPage.xaml.cs]
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace NavigationPageFocusTest;
public partial class MainPage : ContentPage
{
/// <summary>
/// Employee
/// </summary>
public class Employee : INotifyPropertyChanged
{
/// <summary>
/// Property Changed Event
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Property Changed
/// </summary>
/// <param name="name">Property Name</param>
public void OnPropertyChanged([CallerMemberName] string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
/// <summary>
/// Employee Id
/// </summary>
private int pEmployeeId;
/// <summary>
/// Employee Name
/// </summary>
private string pEmployeeName;
/// <summary>
/// Employee Id
/// </summary>
public int EmployeeId
{
get
{
return pEmployeeId;
}
set
{
if (pEmployeeId != value)
{
pEmployeeId = value;
OnPropertyChanged(nameof(EmployeeId));
}
}
}
/// <summary>
/// Employee Name
/// </summary>
public string EmployeeName
{
get
{
return pEmployeeName;
}
set
{
if (pEmployeeName != value)
{
pEmployeeName = value;
OnPropertyChanged(nameof(EmployeeName));
}
}
}
}
/// <summary>
/// MainPage View Model
/// </summary>
public class ViewModelMainPage : INotifyPropertyChanged
{
/// <summary>
/// Property Changed Event
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Property Changed
/// </summary>
/// <param name="name">Property Name</param>
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
/// <summary>
/// Employee List
/// </summary>
private List<Employee> pEmployeeList;
/// <summary>
/// Employee List (Orderd EmployeeId)
/// </summary>
public IEnumerable<Employee> Employees
{
get
{
return pEmployeeList.AsEnumerable()
.OrderBy(
x => x.EmployeeId
);
}
}
/// <summary>
/// Constructor
/// </summary>
public ViewModelMainPage()
{
// Create Employee List
pEmployeeList = new List<Employee>();
}
/// <summary>
/// Add Employee
/// </summary>
/// <param name="employeeId">Employee Id</param>
/// <param name="employeeName">Employee Name</param>
public void AddEmployee(int employeeId, string employeeName)
{
// Add Employee
pEmployeeList.Add(new Employee() { EmployeeId = employeeId, EmployeeName = employeeName });
// Update Employee List
OnPropertyChanged(nameof(Employees));
}
}
/// <summary>
/// Main Page ViewModel
/// </summary>
public ViewModelMainPage vmMainPage = null;
/// <summary>
/// Constructor
/// </summary>
public MainPage()
{
InitializeComponent();
// Create Main Page View Model
vmMainPage = new ViewModelMainPage();
// Add Test Employee
vmMainPage.AddEmployee(1, "Taro Yamada");
vmMainPage.AddEmployee(2, "Hanako Yamada");
// Bind MainPage View Model
this.BindingContext = vmMainPage;
}
/// <summary>
/// Add Employee
/// </summary>
/// <param name="sender">Event Source</param>
/// <param name="e">Event Args</param>
private void btnAdd_Clicked(object sender, EventArgs e)
{
// Set Next Employee Id
int nextId = vmMainPage.Employees.AsEnumerable().Max(x => x.EmployeeId) + 1;
// Create New Employee
vmMainPage.AddEmployee(nextId, null);
// Scroll Employee List
IDispatcherTimer timer;
timer = Dispatcher.CreateTimer();
timer.Interval = new TimeSpan(0, 0, 0, 0, 10);
timer.Tick += (s, e) =>
{
timer.Stop();
MainThread.BeginInvokeOnMainThread(() =>
{
svEmployeeList.ScrollToAsync(slEmployeeList, ScrollToPosition.End, true);
});
};
timer.Start();
}
}
[App.xaml.cs]
MainPage = new NavigationPage(new MainPage());
Below is a video of the verification. (Verification was performed on Android.)
[When using NavigationPage to generate MainPage (.NET MAUI Version)]
https://user-images.githubusercontent.com/125236133/227823981-dc744e88-3ecb-4b7c-9211-01792c30e802.mp4
To reproduce the issue, the Entry should be tapped to focus before pressing the Add Employee button. After pressing the Add Employee button, the Picker at the top of the screen is focused and the Picker Dialog is opened. I don't want this focus shift. In Xamarin.Forms, such focus movement did not occur.
[When using NavigationPage to generate MainPage (Xamarin.Forms Version)]
https://user-images.githubusercontent.com/125236133/227823630-7f2cddc2-c877-469c-b88c-10eb8b1154b5.mp4
If you change MainPage generation to Shell instead of NavigationPage, this focus movement will not occur.
MainPage = new AppShell();
[When using Shell to generate MainPage (.NET MAUI Version)]
https://user-images.githubusercontent.com/125236133/227825027-4668a61d-fbd8-4825-a3ee-fe2ddc771694.mp4
There is an obvious difference in focus control between NavigationPage and Shell. In Xamarin Forms, NavigationPage was used to generate MainPage, so I migrated to .NET MAUI as it is, but I'm having trouble with unintended focus control.
Do you have any good ideas? Thank you.
I have uploaded the code for this issue to github. https://github.com/cat0363/Maui-Issue14211
Since there is no build environment for iOS, operation cannot be verified. Operation verification was performed on Android.
We've added this issue to our backlog, and we will work to address it as time and resources allow. If you have any additional information or questions about this issue, please leave a comment. For additional info about issue management, please read our Triage Process.
Additional Information:
I found a pattern that this problem also occurs on Shell. By sandwiching the screen transition by Navigation on Shell, the same problem that occurred on NavigationPage will occur.
Below is the code needed to reproduce this issue. https://github.com/cat0363/Maui-Issue14211-2
The same problem occurs when the screen transition is sandwiched by the Navigation.PushAsync method even once on Shell.
https://user-images.githubusercontent.com/125236133/228398686-b9449c21-b6a6-4e91-9c72-e12e262cd681.mp4
I've tried looking at focus changes before and after the ViewModel's PropertyChanged event is called. Below is the github source code that I uploaded today.
vmPage2.AddEmployee(nextId, null); // <= here
[NavigationPage] (No page transition) Before : Microsoft.Maui.Platform.MauiAppCompatEditText After : Microsoft.Maui.Platform.MauiPicker
[Shell] (With page transition) Before : Microsoft.Maui.Platform.MauiAppCompatEditText After : Microsoft.Maui.Platform.MauiPicker
[Shell] (No page transition) Before : Microsoft.Maui.Platform.MauiAppCompatEditText After : AndroidX.RecyclerView.Widget.RecyclerView
In Shell when there is no page transition, the focus is moved to RecyclerView after the PropertyChanged event occurs. In other cases the focus is moved to the Picker.
If the focus is cleared, this problem should not occur. I thought of a workaround, albeit for Android only. Define the following methods in the partial class.
public partial class ServiceFocus
{
/// <summary>
/// Clear Focus
/// </summary>
public partial void ClearFocus();
}
Implement a partial class on the Android side as follows.
public partial class ServiceFocus
{
/// <summary>
/// Clear Focus
/// </summary>
public partial void ClearFocus()
{
Android.Views.View view = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity.CurrentFocus;
if (view != null && view is EditText)
{
view.ClearFocus();
}
}
}
Call the following at the beginning of the Clicked event of the Add Employee button.
new ServiceFocus().ClearFocus();
[NavigationPage] (No page transition)
https://user-images.githubusercontent.com/125236133/229020379-24fef04d-8768-4467-8df5-2990af927d51.mp4
[Shell] (With page transition)
https://user-images.githubusercontent.com/125236133/229020392-9e31d43f-c148-4c77-898d-fe45ab4f987d.mp4
[Shell] (No page transition)
https://user-images.githubusercontent.com/125236133/229020792-e1678856-fb12-4e65-bc5b-4adfa178ed1a.mp4
This problem can be avoided by clearing the focus because the focus movement is a problem after the ViewModel property is changed, but it is troublesome to write this on a case-by-case basis.
I've uploaded a workaround for this issue below. https://github.com/cat0363/Maui-Workaround14211
All patterns can be verified by rewriting the github source code. Since there is no build environment, the implementation on the iOS side is empty.
Additional Information: I also verified on the iOS side. This issue does not occur on the iOS side. Occurs only on Android.
Verified this issue with Visual Studio Enterprise 17.7.0 Preview 6.0. Can repro on android platform with sample project.
Maui-Issue14211-main.zip
#14692 seems related
Is #19733 possibly related to this bug? #19733 shows a very simple repro so it might be actually easy to fix.
https://github.com/microsoft/microsoft-ui-xaml/issues/597 was marked as completed in WinAppSDK 1.6.
Once the SDK is released (it's not at the moment AFAIK), it can be tested if it fixes this bug.