Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ object FindBoundItem(ScrollToRequestEventArgs args)
{
if (CollectionViewSource.View[n] is ItemTemplateContext pair)
{
if (pair.Item == args.Item)
if (Equals(pair.Item, args.Item))
{
return CollectionViewSource.View[n];
}
Expand Down
282 changes: 282 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue31351.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows.Input;
using Microsoft.Maui.Controls;

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 31351, "[WinUI] Custom CollectionView does not work on ScrollTo", PlatformAffected.All)]
public partial class Issue31351 : ContentPage
{
readonly Issue31351CustomCollectionView<Issue31351StartupPageModel.Issue31351DisplayItem> _customCollectionView;
readonly Issue31351StartupPageModel _viewModel;

public Issue31351()
{
_viewModel = new Issue31351StartupPageModel();
BindingContext = _viewModel;

var scrollButton = new Button
{
Text = "Scroll to Mid",
AutomationId = "Issue31351ScrollButton"
};
scrollButton.Clicked += (s, e) =>
{
_viewModel.Scroll.Execute(null);
};

var scrollButton2 = new Button
{
Text = "Scroll to Top",
AutomationId = "Issue31351TopScrollButton"
};
scrollButton2.HeightRequest = 50;
scrollButton2.Clicked += (s, e) =>
{
_customCollectionView.ScrollTo(0, position: ScrollToPosition.Center, animate: true);
};

var statusLabel = new Label
{
Text = "Ready - Tap buttons to test ScrollTo functionality",
AutomationId = "Issue31351StatusLabel",
BackgroundColor = Colors.LightYellow,
Padding = new Thickness(10),
HorizontalTextAlignment = TextAlignment.Center
};

_customCollectionView = new Issue31351CustomCollectionView<Issue31351StartupPageModel.Issue31351DisplayItem>
{
AutomationId = "Issue31351CollectionView",
Margin = 10,
BackgroundColor = Colors.LightGray
};
_customCollectionView.SetBinding(Issue31351CustomCollectionView<Issue31351StartupPageModel.Issue31351DisplayItem>.CustomItemsSourceProperty,
new Binding(nameof(Issue31351StartupPageModel.DisplayItems), BindingMode.TwoWay, source: _viewModel));
_customCollectionView.SetBinding(Issue31351CustomCollectionView<Issue31351StartupPageModel.Issue31351DisplayItem>.CustomSelectedItemProperty,
new Binding(nameof(Issue31351StartupPageModel.SelectedDisplayItem), BindingMode.TwoWay, source: _viewModel));
_customCollectionView.SetBinding(Issue31351CustomCollectionView<Issue31351StartupPageModel.Issue31351DisplayItem>.InvalidateCommandProperty,
new Binding(nameof(Issue31351StartupPageModel.InvalidateTreeviewCommand), BindingMode.OneWayToSource, source: _viewModel));


_customCollectionView.Scrolled += (sender, e) =>
{
statusLabel.Text = $"Scrolled: First={e.FirstVisibleItemIndex}, Center={e.CenterItemIndex}, Last={e.LastVisibleItemIndex}";
};


var layoutGrid = new Grid();
layoutGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
layoutGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
layoutGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
layoutGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
layoutGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });

layoutGrid.Add(scrollButton, 0, 0);
layoutGrid.Add(scrollButton2, 0, 1);
layoutGrid.Add(statusLabel, 0, 2);
layoutGrid.Add(_customCollectionView, 0, 3);

Content = layoutGrid;
}

protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing.Execute(null);
}
}

internal class Issue31351StartupPageModel : INotifyPropertyChanged
{
public class Issue31351DisplayItem : IIssue31351TreeviewItem
{
public string Title { get; set; }
}

public List<Issue31351DisplayItem> DisplayItems { get; set; } = new();

Issue31351DisplayItem _displayItem;
public Issue31351DisplayItem SelectedDisplayItem
{
get { return _displayItem; }
set { _displayItem = value; OnPropertyChanged("SelectedDisplayItem"); }
}

public Action InvalidateTreeviewCommand { get; set; }

public class Issue31351DataItem
{
public int Id { get; set; }
public string Name { get; set; }
}

public List<Issue31351DataItem> data = new();

public Issue31351StartupPageModel()
{
#region *// Generate test data
for (int i = 0; i < 100; i++)
{
data.Add(new Issue31351DataItem { Id = i, Name = $"Item {i}" });
}
#endregion
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}

public ICommand OnAppearing => new Command(() =>
{
DisplayItems.Clear();
foreach (var dataItem in data)
{
DisplayItems.Add(new Issue31351DisplayItem { Title = dataItem.Name });
}

SelectedDisplayItem = DisplayItems[5];
InvalidateTreeviewCommand?.Invoke();
});

public ICommand Scroll => new Command(() =>
{
SelectedDisplayItem = DisplayItems[50];
InvalidateTreeviewCommand?.Invoke();
});
}

public interface IIssue31351TreeviewItem
{
string Title { get; set; }
}

internal class Issue31351CustomCollectionView<T> : CollectionView where T : IIssue31351TreeviewItem
{
#region *// Bindable properties

#region *// Item source
public static readonly BindableProperty CustomItemsSourceProperty = BindableProperty.Create(
nameof(CustomItemsSource),
typeof(List<T>),
typeof(Issue31351CustomCollectionView<T>),
null);

public List<T> CustomItemsSource
{
get { return (List<T>)GetValue(CustomItemsSourceProperty); }
set { SetValue(CustomItemsSourceProperty, value); }
}
#endregion

#region *// InvalidateCommand
public static BindableProperty InvalidateCommandProperty = BindableProperty.Create(
nameof(InvalidateCommand),
typeof(Action),
typeof(Issue31351CustomCollectionView<T>),
null,
BindingMode.OneWayToSource);

public Action InvalidateCommand
{
get { return (Action)GetValue(InvalidateCommandProperty); }
set { SetValue(InvalidateCommandProperty, value); }
}
#endregion

#region *// Selected item
public static readonly BindableProperty CustomSelectedItemProperty = BindableProperty.Create(
nameof(CustomSelectedItem),
typeof(IIssue31351TreeviewItem),
typeof(Issue31351CustomCollectionView<T>),
null);

public IIssue31351TreeviewItem CustomSelectedItem
{
get { return (IIssue31351TreeviewItem)GetValue(CustomSelectedItemProperty); }
set { SetValue(CustomSelectedItemProperty, value); }
}
#endregion

#endregion

public class Issue31351CollectionViewItem
{
public string Title { get; set; }

public override bool Equals(object obj)
{
if (obj == null)
return false;
if (obj is not Issue31351CollectionViewItem)
return false;
return ((Issue31351CollectionViewItem)this).Title.Equals(((Issue31351CollectionViewItem)obj).Title, StringComparison.Ordinal);
}

public override int GetHashCode()
{
return Title?.GetHashCode(StringComparison.Ordinal) ?? 0;
}
}

List<Issue31351CollectionViewItem> CollectionViewItems = new();

public Issue31351CustomCollectionView()
{
ItemTemplate = new DataTemplate(() =>
{
var label = new Label();
label.SetBinding(Label.TextProperty, nameof(Issue31351CollectionViewItem.Title));
label.Padding = new Thickness(10);
label.BackgroundColor = Colors.White;
Comment on lines +236 to +238
Copy link
Preview

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

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

The Label element lacks an AutomationId which is required for UI test automation. According to the guidelines, AutomationIds should be UNIQUE and are necessary for WaitForElement to find the correct element in tests.

Copilot uses AI. Check for mistakes.


var border = new Border
{
Content = label,
BackgroundColor = Colors.White,
Stroke = Colors.Gray,
StrokeThickness = 1,
Margin = new Thickness(2)
};

return border;
});

SelectionMode = SelectionMode.Single;

InvalidateCommand = new Action(() =>
{
Invalidate();
});
}

void Invalidate()
{
CollectionViewItems.Clear();

if (CustomItemsSource != null)
{
foreach (var itemSource in CustomItemsSource)
{
CollectionViewItems.Add(new Issue31351CollectionViewItem { Title = itemSource.Title });
}
}

ItemsSource = CollectionViewItems;
if (CustomSelectedItem != null)
{
var selectedCollectionViewItem = CollectionViewItems.FirstOrDefault(o => o.Title == CustomSelectedItem.Title);

ScrollTo(selectedCollectionViewItem, position: ScrollToPosition.Center, animate: true);
SelectedItem = selectedCollectionViewItem;
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue31351 : _IssuesUITest
{
public Issue31351(TestDevice testDevice) : base(testDevice)
{
}

public override string Issue => "[WinUI] Custom CollectionView does not work on ScrollTo";

[Test]
[Category(UITestCategories.CollectionView)]
public void CustomCollectionViewShouldScroll()
{
App.WaitForElement("Issue31351CollectionView");
App.WaitForElement("Issue31351ScrollButton");
App.Tap("Issue31351ScrollButton");
var MidItemRect = App.WaitForElement("Item 50").GetRect();
Copy link
Preview

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

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

This test relies on finding elements by text content ('Item 50') rather than AutomationId. This approach is brittle because it depends on the specific text generation logic. The corresponding UI elements in the HostApp should have unique AutomationIds for more reliable test automation.

Copilot uses AI. Check for mistakes.

Assert.That(MidItemRect.X, Is.GreaterThanOrEqualTo(0));
App.WaitForElement("Issue31351TopScrollButton");
App.Tap("Issue31351TopScrollButton");
var TopItemRect = App.WaitForElement("Item 1").GetRect();
Copy link
Preview

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

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

Similar to the previous issue, this test relies on text content ('Item 1') to locate elements instead of using AutomationIds. This makes the test fragile and dependent on the exact text formatting.

Copilot uses AI. Check for mistakes.

Assert.That(TopItemRect.X, Is.GreaterThanOrEqualTo(0));
}
}
Loading