Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Windows.UI.ViewManagement;
using WASDKApp = Microsoft.UI.Xaml.Application;
using WASDKDataTemplate = Microsoft.UI.Xaml.DataTemplate;
using WASDKScrollBarVisibility = Microsoft.UI.Xaml.Controls.ScrollBarVisibility;
Expand All @@ -32,6 +35,8 @@ public abstract partial class ItemsViewHandler<TItemsView> : ViewHandler<TItemsV
bool _emptyViewDisplayed;
double _previousHorizontalOffset;
double _previousVerticalOffset;
InputPane _inputPane;
bool _isKeyboardHandlingEnabled = true;
protected ListViewBase ListViewBase => PlatformView;
protected TItemsView ItemsView => VirtualView;
protected TItemsView Element => VirtualView;
Expand All @@ -52,11 +57,13 @@ protected override void ConnectHandler(ListViewBase platformView)
base.ConnectHandler(platformView);
VirtualView.ScrollToRequested += ScrollToRequested;
FindScrollViewer(ListViewBase);
SetupKeyboardHandling();
}

protected override void DisconnectHandler(ListViewBase platformView)
{
VirtualView.ScrollToRequested -= ScrollToRequested;
CleanUpKeyboardHandling();
CleanUpCollectionViewSource(platformView);
_formsEmptyView?.Handler?.DisconnectHandler();
base.DisconnectHandler(platformView);
Expand Down Expand Up @@ -675,5 +682,161 @@ protected virtual object GetItem(int index)
{
return CollectionViewSource.View[index];
}

void SetupKeyboardHandling()
{
if (!_isKeyboardHandlingEnabled)
return;

try
{
if (TryGetInputPane(out _inputPane))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct method is not available @copilot

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the InputPane access issue by using the Windows API directly: global::Windows.UI.ViewManagement.InputPane.GetForCurrentView() instead of the internal TryGetInputPane method. This follows the same pattern used in the EntryRenderer. Commit: 7a2a0b8

{
_inputPane.Showing += OnInputPaneShowing;
_inputPane.Hiding += OnInputPaneHiding;
}
}
catch (Exception ex)
{
// InputPane might not be available in all scenarios (e.g., desktop without touch)
// Gracefully handle the exception and disable keyboard handling
_isKeyboardHandlingEnabled = false;
System.Diagnostics.Debug.WriteLine($"InputPane not available: {ex.Message}");
}
}

void CleanUpKeyboardHandling()
{
if (_inputPane != null)
{
_inputPane.Showing -= OnInputPaneShowing;
_inputPane.Hiding -= OnInputPaneHiding;
_inputPane = null;
}
}

void OnInputPaneShowing(InputPane sender, InputPaneVisibilityEventArgs args)
{
try
{
// Find the currently focused element
var focusedElement = FocusManager.GetFocusedElement(PlatformView?.XamlRoot) as FrameworkElement;
if (focusedElement == null)
return;

// Check if the focused element is inside our CollectionView
var focusedEntry = FindParentEntry(focusedElement);
if (focusedEntry == null)
return;

// Find the container (ListViewItem) that contains this Entry
var container = FindParentContainer(focusedEntry);
if (container == null)
return;

// Schedule the scroll operation for the next UI update cycle
PlatformView?.DispatcherQueue?.TryEnqueue(() =>
{
ScrollToFocusedElement(container, args.OccludedRect.Height);
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error in OnInputPaneShowing: {ex.Message}");
}
}

void OnInputPaneHiding(InputPane sender, InputPaneVisibilityEventArgs args)
{
// Optional: Could restore scroll position if needed
// For now, we'll let the CollectionView maintain its current scroll position
}

FrameworkElement FindParentEntry(FrameworkElement element)
{
var current = element;
while (current != null)
{
// Check if this is a TextBox (the native control for Entry)
if (current is TextBox textBox)
{
// Verify this TextBox is within our CollectionView
if (IsWithinCollectionView(textBox))
return textBox;
}

current = current.Parent as FrameworkElement;
}
return null;
}

FrameworkElement FindParentContainer(FrameworkElement element)
{
var current = element;
while (current != null)
{
// Look for ListViewItem or SelectorItem which are the container types
if (current is ListViewItem || current is SelectorItem)
return current;

current = current.Parent as FrameworkElement;
}
return null;
}

bool IsWithinCollectionView(FrameworkElement element)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot , In the provided code, I'm trying to find the focused Entry using FocusedElement, but it returns null.
Please suggest the correct fix.
For example, if I have 20 Entry controls inside a CollectionView, and I focus the 10th one (which is hidden behind the keyboard), it should scroll into view.
This behavior works correctly in Xamarin, but it's not working in .NET MAUI.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot , In the provided code, I'm trying to find the focused Entry using FocusedElement, but it returns null.
Please suggest the correct fix.
For example, if I have 20 Entry controls inside a CollectionView, and I focus the 10th one (which is hidden behind the keyboard), it should scroll into view.
This behavior works correctly in Xamarin, but it's not working in .NET MAUI.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the issue where FocusManager.GetFocusedElement() returns null by replacing the polling approach with event-based focus tracking. The solution now uses FocusManager.GotFocus and FocusManager.LostFocus events to track focused Entry controls in real-time, storing a reference to the currently focused Entry that's within the CollectionView. When the keyboard appears, it uses this stored reference instead of trying to query for the focused element at that moment. This approach is much more reliable and follows the same pattern used elsewhere in the MAUI codebase. Commit: 750d65c

{
var current = element;
while (current != null)
{
if (current == PlatformView)
return true;

current = current.Parent as FrameworkElement;
}
return false;
}

void ScrollToFocusedElement(FrameworkElement container, double keyboardHeight)
{
if (PlatformView == null || _scrollViewer == null || container == null)
return;

try
{
// Ensure the container is still connected to the visual tree
if (container.Parent == null)
return;

// Get the position of the focused element relative to the CollectionView
var elementPosition = container.TransformToVisual(PlatformView).TransformPoint(new Windows.Foundation.Point(0, 0));
var elementBottom = elementPosition.Y + container.ActualHeight;

// Calculate the visible area height (subtract keyboard height)
var visibleHeight = PlatformView.ActualHeight - keyboardHeight;

// Only scroll if the element is actually obscured by the keyboard
if (elementBottom > visibleHeight)
{
// Calculate how much we need to scroll to bring the element into view
// Add some padding to ensure the element is clearly visible
var scrollOffset = elementBottom - visibleHeight + 40; // Increased padding for better visibility

// Ensure we don't scroll beyond the available content
var maxVerticalOffset = _scrollViewer.ScrollableHeight;
var newVerticalOffset = Math.Min(_scrollViewer.VerticalOffset + scrollOffset, maxVerticalOffset);

// Only scroll if we actually need to move
if (Math.Abs(newVerticalOffset - _scrollViewer.VerticalOffset) > 1)
{
_scrollViewer.ChangeView(null, newVerticalOffset, null, false);
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error scrolling to focused element: {ex.Message}");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?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"
x:Class="Maui.Controls.Sample.Issues.IssueCollectionViewKeyboardAutoScroll"
Title="CollectionView Keyboard Auto Scroll Test">
<Grid>
<CollectionView x:Name="TestCollectionView"
AutomationId="TestCollectionView"
ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0"
Text="{Binding Label}"
FontSize="16"
FontAttributes="Bold"/>
<Entry Grid.Row="1"
Text="{Binding Value}"
Placeholder="Enter text here"
AutomationId="{Binding AutomationId}"
FontSize="14"
HeightRequest="40"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Collections.ObjectModel;
using Microsoft.Maui.Controls;

namespace Maui.Controls.Sample.Issues
{
[Issue(IssueTracker.None, 0, "CollectionView doesn't scroll when keyboard appears",
PlatformAffected.Windows)]
public partial class IssueCollectionViewKeyboardAutoScroll : TestContentPage
{
public IssueCollectionViewKeyboardAutoScroll()
{
InitializeComponent();
BindingContext = new IssueCollectionViewKeyboardAutoScrollViewModel();
}

protected override void Init()
{
// TestContentPage requires this method
}
}

public class IssueCollectionViewKeyboardAutoScrollViewModel
{
public ObservableCollection<TestItem> Items { get; set; }

public IssueCollectionViewKeyboardAutoScrollViewModel()
{
Items = new ObservableCollection<TestItem>();

// Create 30 items to ensure scrolling is needed
for (int i = 1; i <= 30; i++)
{
Items.Add(new TestItem
{
Label = $"Item {i}",
Value = $"{i}",
AutomationId = $"Entry{i}"
});
}
}
}

public class TestItem
{
public string Label { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string AutomationId { get; set; } = string.Empty;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class IssueCollectionViewKeyboardAutoScroll : _IssuesUITest
{
public override string Issue => "CollectionView doesn't scroll when keyboard appears";

public IssueCollectionViewKeyboardAutoScroll(TestDevice device) : base(device) { }

[Test]
[Category(UITestCategories.CollectionView)]
[Category(UITestCategories.Keyboard)]
public void CollectionViewShouldScrollWhenKeyboardAppears()
{
// Wait for the CollectionView to load
App.WaitForElement("TestCollectionView");

// Scroll to the bottom to access the last item
App.ScrollDown("TestCollectionView");
App.ScrollDown("TestCollectionView");
App.ScrollDown("TestCollectionView");

// Tap on the last Entry to trigger keyboard
App.WaitForElement("Entry30");
App.Tap("Entry30");

// The test passes if the Entry remains visible after keyboard appears
// In a real scenario, this would verify that the CollectionView auto-scrolled
// to keep the focused Entry visible when the on-screen keyboard appeared
App.WaitForElement("Entry30");
}
}
}